Compare commits

..

610 Commits

Author SHA1 Message Date
Dan Albert
59179935a5 Refactor groundobjectsgen.
In preparation for deduping the building TGOs.
2020-10-29 00:57:04 -07:00
Dan Albert
670dd33ae1 Refactor game generation.
No real functional improvements yet, but want to get this submitted to
unblock Skynet changes.
2020-10-28 22:53:46 -07: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
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
671 changed files with 44586 additions and 7440 deletions

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

@@ -0,0 +1,54 @@
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: Build binaries
run: |
./venv/scripts/activate
$env:PYTHONPATH=".;./pydcs"
pyinstaller pyinstaller.spec
- uses: actions/upload-artifact@v2
with:
name: dcs_liberation
path: dist/

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

@@ -0,0 +1,122 @@
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: 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

11
.gitignore vendored
View File

@@ -8,6 +8,15 @@ logs.txt
dist/**
a.py
resources/tools/a.miz
tests/**
# User-specific stuff
.idea/
/kneeboards
/liberation_preferences.json
/state.json
logs/
qt_ui/logs/liberation.log
*.psd

4
.gitmodules vendored Normal file
View File

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

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"
}
]
}

4
.vscode/settings.json vendored Normal file
View File

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

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,12 +1,36 @@
![Logo](https://i.imgur.com/c2k18E1.png)
[DCS World](https://www.digitalcombatsimulator.com/en/products/world/) single-player semi dynamic campaign.
[![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)
DCS Liberation uses [pydcs](http://github.com/pydcs/dcs) for mission generation
and [Mist](https://github.com/mrSkortch/MissionScriptingTools) for mission scripting
[![Download](https://img.shields.io/github/downloads/khopa/dcs_liberation/total?label=Download)](https://github.com/Khopa/dcs_liberation/releases)
[![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 semi dynamic campaign.
It is an external program that generates full and complex DCS missions and manage a persistent combat environment.
![Logo](https://imgur.com/B6tvlBJ.png)
## Downloads
Latest release is available here : https://github.com/Khopa/dcs_liberation/releases
## Resources
* [Getting Started](https://github.com/Khopa/dcs_liberation/wiki/Getting-started)
Tutorials, contributors and developer's guides are available in the project's [Wiki](https://github.com/Khopa/dcs_liberation/wiki/)
* [Tutorials](https://github.com/Khopa/dcs_liberation/wiki/Tutorial-01-:-UI)
## 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.
And for the JTAC feature, DCS Liberation embed Ciribob's JTAC Autolase [script](https://github.com/ciribob/DCS-JTACAutoLaze).
Please also show some support to these projects !

View File

@@ -1,64 +0,0 @@
#!/usr/bin/env python3
import logging
import os
import re
import sys
import dcs
import ui.corruptedsavemenu
import ui.mainmenu
import ui.newgamemenu
import ui.window
from game.game import Game
from userdata import persistency, logging as logging_module
assert len(sys.argv) >= 3, "__init__.py should be started with two mandatory arguments: %UserProfile% location and application version"
persistency.setup(sys.argv[1])
dcs.planes.FlyingType.payload_dirs = [os.path.join(os.path.dirname(os.path.realpath(__file__)), "resources\\payloads")]
VERSION_STRING = sys.argv[2]
logging_module.setup_version_string(VERSION_STRING)
logging.info("Using {} as userdata folder".format(persistency.base_path()))
def proceed_to_main_menu(game: Game):
m = ui.mainmenu.MainMenu(w, None, game)
m.display()
def is_version_compatible(save_version):
current_version_components = re.split(r"[\._]", VERSION_STRING)
save_version_components = re.split(r"[\._]", save_version)
if "--ignore-save" in sys.argv:
return False
if current_version_components == save_version_components:
return True
if save_version in ["1.4_rc1", "1.4_rc2", "1.4_rc3", "1.4_rc4", "1.4_rc5", "1.4_rc6"]:
return False
if current_version_components[:2] == save_version_components[:2]:
return True
return False
w = ui.window.Window()
try:
game = persistency.restore_game()
if not game or not is_version_compatible(game.settings.version):
ui.newgamemenu.NewGameMenu(w, w.start_new_game).display()
else:
game.settings.version = VERSION_STRING
proceed_to_main_menu(game)
except Exception as e:
logging.exception(e)
ui.corruptedsavemenu.CorruptedSaveMenu(w).display()
w.run()

View File

@@ -1,39 +1,274 @@
#2.0 RC 7
# 2.2.X
## Features/Improvements :
* **[Flight Planner]** Flight planner overhaul, with package and TOT system
* **[Map]** Highlight the selected flight path on the map
* **[Map]** Improved flight plan display settings
* **[Map]** Improved SAM display settings
* **[Map]** Added polygon debug mode display
* **[New Game]** Starting budget can be freely selected
* **[Moddability]** Custom campaigns can be designed through json files
## Fixes :
* **[Campaign generator]** Ship group and offshore buildings should not be generated on land anymore
* **[UI]** Missing TER weapons in custom payload now selectable.
# 2.1.5
## 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 :
##Features/Improvements :
* **[Units/Factions]** Added P-47D-30 for factions allies_1944
* **[Units/Factions]** Replaced S3-B Tanker by KC130 for most factions
* **[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 (Added a new setting)
* **[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
* **[Info Panel]** Added information about units destroyed outside the frontline in the debriefing window
* **[Info Panel]** Added information about buildings destroyed in the debriefing window
* **[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
* **[Misc]** Made it possible to setup DCS Saved Games directory and DCS installation directory manually
* **[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
##Fixed issues :
* **[Mission Generator]** When playing as RED the activation trigger would not be properly generated
* **[Mission Generator]** Changed "strike" payload for Su-24M that was innefective
* **[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]** FW-190A8 is now properly considered as a flyable
* **[Maps/Campaign]** Now using Vasiani airport instead of Soganlung in North Caucasus campaign (More parking slots)
* **[Info Panel]** Message displayed on base capture event stated that the ennemy captured an airbase, while it was the player who captured it.
* **[Map]** Graphical glitch on map when one building of an objective was destroyed, but not the others
* **[Map]** Change power station template. (Buildings could end up superposed).
* **[Mission Generator]** Change power station template. (Buildings could end up superposed).
#2.0 RC 6
* **[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 :
## 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
@@ -47,11 +282,11 @@ Sorry :(
* **[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 :
## 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)
* **[Naming]** CAP mission flown from airbase are not named BARCAP anymore (CAP from carrier is still named BARCAP)

View File

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

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,16 @@
import inspect
import dcs
DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick', 'aa']
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp', 'aa']
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'aa']
FORTIFICATION_BUILDINGS = ['Siegfried Line', 'Concertina wire', 'Concertina Wire', 'Czech hedgehogs 1', 'Czech hedgehogs 2',
'Dragonteeth 1', 'Dragonteeth 2', 'Dragonteeth 3', 'Dragonteeth 4', 'Dragonteeth 5',
'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,51 @@
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_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,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
Bf_109K_4,
FW_190D9,
FW_190A8,
I_16,
]

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

@@ -0,0 +1,101 @@
from dataclasses import dataclass
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
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_min_track_length: int
cap_max_track_length: int
cap_min_distance_from_cp: int
cap_max_distance_from_cp: int
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),
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_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),
)
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),
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_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),
)
WWII_DOCTRINE = Doctrine(
cap=True,
cas=True,
sead=False,
strike=True,
antiship=True,
strike_max_range=1500000,
sead_max_range=1500000,
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_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),
)

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
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,11 @@
import json
import logging
import os
import re
import threading
import time
import typing
from dcs.lua import parse
from dcs.mission import Mission
from dcs.unit import UnitType
from game import db
from .persistency import base_path
DEBRIEFING_LOG_EXTENSION = "log"
@@ -35,12 +29,19 @@ class Debriefing:
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"]
print(self.base_capture_events)
print(self.killed_aircrafts)
print(self.killed_ground_units)
print(self.weapons_fired)
print(self.mission_ended)
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)
@@ -59,7 +60,7 @@ class Debriefing:
if type is not None:
self.dead_aircraft.append(aircraft)
except Exception as e:
print(e)
logging.error(e)
for unit in self.killed_ground_units:
try:
@@ -70,13 +71,14 @@ class Debriefing:
if type is not None:
self.dead_units.append(unit)
except Exception as e:
print(e)
logging.error(e)
for unit in self.killed_ground_units:
for cp in game.theater.controlpoints:
print(cp.name)
print(cp.captured)
logging.info(cp.name)
logging.info(cp.captured)
if cp.captured:
country = self.player_country_id
else:
@@ -84,8 +86,8 @@ class Debriefing:
player_unit = (country == self.player_country_id)
for i, ground_object in enumerate(cp.ground_objects):
print(unit)
print(ground_object.string_identifier)
logging.info(unit)
logging.info(ground_object.string_identifier)
if ground_object.matches_string_identifier(unit):
unit = DebriefingDeadUnitInfo(country, player_unit, ground_object.dcs_identifier)
self.dead_buildings.append(unit)
@@ -103,10 +105,10 @@ class Debriefing:
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]
print(self.player_dead_aircraft)
print(self.enemy_dead_aircraft)
print(self.player_dead_units)
print(self.enemy_dead_units)
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:
@@ -150,29 +152,50 @@ class Debriefing:
else:
self.enemy_dead_buildings_dict[a.type] = 1
print("DEBRIEFING PRE PROCESS")
print(self.player_dead_aircraft_dict)
print(self.enemy_dead_aircraft_dict)
print(self.player_dead_units_dict)
print(self.enemy_dead_units_dict)
print(self.player_dead_buildings_dict)
print(self.enemy_dead_buildings_dict)
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)
def _poll_new_debriefing_log(callback: typing.Callable, game):
if os.path.isfile("state.json"):
last_modified = os.path.getmtime("state.json")
else:
last_modified = 0
while True:
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.parse(os.path.join(debriefing_directory_location(), file))
debriefing = Debriefing(json_data, game)
callback(debriefing)
break
time.sleep(5)
class PollDebriefingFileThread(threading.Thread):
"""Thread class with a stop() method. The thread itself has to check
regularly for the stopped() condition."""
def wait_for_debriefing(callback: typing.Callable, game):
threading.Thread(target=_poll_new_debriefing_log, args=[callback, game]).start()
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,24 +1,22 @@
import typing
from __future__ import annotations
import logging
import math
from typing import Dict, List, Optional, Type, TYPE_CHECKING
from dcs.action import Coalition
from dcs.unittype import UnitType
from dcs.task import *
from dcs.vehicles import AirDefence
from dcs.mapping import Point
from dcs.task import Task
from dcs.unittype import UnitType
from game import *
from game import db, persistency
from game.debriefing import Debriefing
from game.infos.information import Information
from theater import *
from gen.environmentgen import EnvironmentSettings
from gen.conflictgen import Conflict
from game.db import assigned_units_from, unitdict_from
from theater.start_generator import generate_airbase_defense_group
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
import game.db as db
if TYPE_CHECKING:
from ..game import Game
DIFFICULTY_LOG_BASE = 1.1
EVENT_DEPARTURE_MAX_DISTANCE = 340000
@@ -28,6 +26,7 @@ MINOR_DEFEAT_INFLUENCE = 0.1
DEFEAT_INFLUENCE = 0.3
STRONG_DEFEAT_INFLUENCE = 0.5
class Event:
silent = False
informational = False
@@ -37,17 +36,15 @@ class Event:
game = None # type: Game
location = None # type: Point
from_cp = None # type: ControlPoint
departure_cp = None # type: ControlPoint
to_cp = None # type: ControlPoint
operation = None # type: Operation
difficulty = 1 # type: int
environment_settings = None # type: EnvironmentSettings
BONUS_BASE = 5
def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str):
self.game = game
self.departure_cp = None
self.departure_cp: Optional[ControlPoint] = None
self.from_cp = from_cp
self.to_cp = target_cp
self.location = location
@@ -59,29 +56,14 @@ class Event:
return self.attacker_name == self.game.player_name
@property
def enemy_cp(self) -> ControlPoint:
def enemy_cp(self) -> Optional[ControlPoint]:
if self.attacker_name == self.game.player_name:
return self.to_cp
else:
return self.departure_cp
@property
def threat_description(self) -> str:
return ""
def flight_name(self, for_task: typing.Type[typing.Type[Task]]) -> str:
return "Flight"
@property
def tasks(self) -> typing.Collection[typing.Type[Task]]:
return []
@property
def ai_banned_tasks(self) -> typing.Collection[typing.Type[Task]]:
return []
@property
def player_banned_tasks(self) -> typing.Collection[typing.Type[Task]]:
def tasks(self) -> List[Type[Task]]:
return []
@property
@@ -106,18 +88,6 @@ class Event:
def is_successfull(self, debriefing: Debriefing) -> bool:
return self.operation.is_successfull(debriefing)
def player_attacking(self, cp: ControlPoint, flights: db.TaskForceDict):
if self.is_player_attacking:
self.departure_cp = cp
else:
self.to_cp = cp
def player_defending(self, cp: ControlPoint, flights: db.TaskForceDict):
if self.is_player_attacking:
self.departure_cp = cp
else:
self.to_cp = cp
def generate(self):
self.operation.is_awacs_enabled = self.is_awacs_enabled
self.operation.ca_slots = self.ca_slots
@@ -193,9 +163,12 @@ class Event:
for i, ground_object in enumerate(cp.ground_objects):
if ground_object.dcs_identifier in ["AA", "CARRIER", "LHA"]:
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])
@@ -206,10 +179,10 @@ class Event:
# ------------------------------
# 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
#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:
@@ -221,29 +194,19 @@ class Event:
if cp.id == id:
if cp.captured and new_owner_coalition != coalition:
cp.captured = False
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)
pname = self.game.enemy_name
captured_cps.append(cp)
elif not(cp.captured) and new_owner_coalition == coalition:
cp.captured = True
for_player = True
info = Information(cp.name + " captured !", "We took control of " + cp.name + "! Great job !", self.game.turn)
self.game.informations.append(info)
pname = self.game.player_name
captured_cps.append(cp)
else:
continue
cp.base.aircraft = {}
cp.base.armor = {}
airbase_def_id = 0
for g in cp.ground_objects:
g.groups = []
if g.airbase_group and pname != "":
generate_airbase_defense_group(airbase_def_id, g, pname, self.game, cp)
airbase_def_id = airbase_def_id + 1
cp.capture(self.game, for_player)
for cp in captured_cps:
logging.info("Will run redeploy for " + cp.name)
@@ -253,6 +216,11 @@ class Event:
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():
@@ -260,7 +228,7 @@ class Event:
for enemy_cp in enemy_cps:
print("Compute frontline progression for : " + cp.name + " to " + enemy_cp.name)
delta = 0
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]
@@ -274,6 +242,8 @@ class Event:
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
@@ -296,11 +266,21 @@ class Event:
else:
delta = DEFEAT_INFLUENCE
elif ally_casualties > enemy_casualties:
player_won = False
if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH:
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:
delta = STRONG_DEFEAT_INFLUENCE
# 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]:
@@ -320,7 +300,7 @@ class Event:
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,
"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)
@@ -371,7 +351,6 @@ class Event:
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__(game=game,
@@ -381,12 +360,12 @@ class UnitsDeliveryEvent(Event):
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

View File

@@ -1,22 +1,17 @@
from game.event import *
from typing import List, Type
from dcs.task import CAP, CAS, Task
from game import db
from game.operation.frontlineattack import FrontlineAttackOperation
from userdata.debriefing import Debriefing
from .event import Event
from ..debriefing import Debriefing
class FrontlineAttackEvent(Event):
TARGET_VARIETY = 2
TARGET_AMOUNT_FACTOR = 0.5
ATTACKER_AMOUNT_FACTOR = 0.4
ATTACKER_DEFENDER_FACTOR = 0.7
STRENGTH_INFLUENCE = 0.3
SUCCESS_FACTOR = 1.5
@property
def threat_description(self):
return "{} vehicles".format(self.to_cp.base.assemble_count())
@property
def tasks(self) -> typing.Collection[typing.Type[Task]]:
def tasks(self) -> List[Type[Task]]:
if self.is_player_attacking:
return [CAS, CAP]
else:
@@ -26,32 +21,11 @@ class FrontlineAttackEvent(Event):
def global_cp_available(self) -> bool:
return True
def flight_name(self, for_task: typing.Type[Task]) -> str:
if for_task == CAS:
return "CAS flight"
elif for_task == CAP:
return "CAP flight"
elif for_task == PinpointStrike:
return "Ground attack"
def __str__(self):
return "Frontline attack"
def is_successfull(self, debriefing: Debriefing):
if self.game.player_name == self.attacker_name:
attacker_country = self.game.player_country
defender_country = self.game.enemy_country
else:
attacker_country = self.game.enemy_country
defender_country = self.game.player_country
# TODO : Rework
#alive_attackers = sum([v for k, v in debriefing.alive_units.get(attacker_country, {}).items() if db.unit_task(k) == PinpointStrike])
#alive_defenders = sum([v for k, v in debriefing.alive_units.get(defender_country, {}).items() if db.unit_task(k) == PinpointStrike])
#attackers_success = (float(alive_attackers) / (alive_defenders + 0.01)) > self.SUCCESS_FACTOR
attackers_success = True
if self.from_cp.captured:
return attackers_success
else:
@@ -65,46 +39,11 @@ class FrontlineAttackEvent(Event):
self.to_cp.base.affect_strength(-0.1)
def player_attacking(self, flights: db.TaskForceDict):
# assert CAS in flights and CAP in flights and len(flights) == 2, "Invalid flights"
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)
defenders = self.to_cp.base.assemble_attack()
max_attackers = int(math.ceil(sum(defenders.values()) * self.ATTACKER_DEFENDER_FACTOR))
attackers = db.unitdict_restrict_count(self.from_cp.base.assemble_attack(), max_attackers)
op.setup(defenders=defenders,
attackers=attackers,
strikegroup=flights[CAS],
escort=flights[CAP],
interceptors=assigned_units_from(self.to_cp.base.scramble_interceptors(1)))
self.operation = op
def player_defending(self, flights: db.TaskForceDict):
# assert CAP in flights and len(flights) == 1, "Invalid flights"
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)
defenders = self.to_cp.base.assemble_attack()
max_attackers = int(math.ceil(sum(defenders.values())))
attackers = db.unitdict_restrict_count(self.from_cp.base.assemble_attack(), max_attackers)
op.setup(defenders=defenders,
attackers=attackers,
strikegroup=assigned_units_from(self.from_cp.base.scramble_cas(1)),
escort=assigned_units_from(self.from_cp.base.scramble_sweep(1)),
interceptors=flights[CAP])
self.operation = op

View File

@@ -1,78 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
BLUEFOR_MODERN = {
"country": "USA",
"side": "blue",
"units": [
F_15C,
F_14B,
FA_18C_hornet,
F_16C_50,
JF_17,
M_2000C,
F_5E_3,
Su_27,
Su_25T,
A_10A,
A_10C,
AV8BNA,
AJS37,
KC_135,
KC130,
C_130,
E_3A,
UH_1H,
AH_64D,
Ka_50,
Armor.MBT_M1A2_Abrams,
Armor.MBT_Leopard_2,
Armor.ATGM_M1134_Stryker,
Armor.IFV_M2A2_Bradley,
Armor.IFV_Marder,
Armor.APC_M1043_HMMWV_Armament,
Artillery.MLRS_M270,
Artillery.SPH_M109_Paladin,
Unarmed.Transport_M818,
Infantry.Infantry_M4,
Infantry.Soldier_M249,
AirDefence.SAM_Hawk_PCP,
AirDefence.SAM_Patriot_EPP_III,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Armed_speedboat,
], "shorad": [
AirDefence.SAM_Avenger_M1097,
], "aircraft_carrier": [
CVN_74_John_C__Stennis,
], "helicopter_carrier": [
LHA_1_Tarawa,
], "destroyer": [
Oliver_Hazzard_Perry_class,
USS_Arleigh_Burke_IIa,
], "cruiser": [
Ticonderoga_class,
], "carrier_names": [
"CVN-71 Theodore Roosevelt",
"CVN-72 Abraham Lincoln",
"CVN-73 George Washington",
"CVN-74 John C. Stennis",
], "lhanames": [
"LHA-1 Tarawa",
"LHA-2 Saipan",
"LHA-3 Belleau Wood",
"LHA-4 Nassau",
"LHA-5 Peleliu"
]
}

View File

@@ -1,63 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
China_2000 = {
"country": "China",
"side": "red",
"units": [
MiG_21Bis, # Standing as J-7
Su_30,
Su_33,
J_11A,
JF_17,
IL_76MD,
IL_78M,
An_26B,
An_30M,
Yak_40,
A_50,
Mi_8MT,
AirDefence.SAM_SA_10_S_300PS_LN_5P85C, # Standing as HQ-9+
AirDefence.SAM_SA_6_Kub_LN_2P25,
AirDefence.HQ_7_Self_Propelled_LN,
Armor.ZTZ_96B,
Armor.MBT_T_55,
Armor.ZBD_04A,
Armor.IFV_BMP_1,
Artillery.MLRS_9A52_Smerch,
Artillery.SPH_2S9_Nona,
Unarmed.Transport_Ural_375,
Unarmed.Transport_UAZ_469,
Infantry.Soldier_AK,
Infantry.Paratrooper_RPG_16,
CV_1143_5_Admiral_Kuznetsov,
Bulk_cargo_ship_Yakushev,
Dry_cargo_ship_Ivanov,
Tanker_Elnya_160
],
"shorad":[
AirDefence.SPAAA_ZSU_23_4_Shilka,
AirDefence.Rapier_FSA_Launcher, # Standing as PL-9C Shorad
AirDefence.HQ_7_Self_Propelled_LN
], "aircraft_carrier": [
CV_1143_5_Admiral_Kuznetsov,
], "destroyer": [
Type_052B_Destroyer,
Type_052C_Destroyer
], "cruiser": [
Type_054A_Frigate,
], "carrier_names": [
"001 Liaoning",
"002 Shandong",
]
}

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

@@ -0,0 +1,252 @@
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
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 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)
@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.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 == "ww2ally":
faction.building_set = WW2_ALLIES_BUILDINGS
elif building_set == "ww2germany":
faction.building_set = WW2_GERMANY_BUILDINGS
else:
faction.building_set = DEFAULT_AVAILABLE_BUILDINGS
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,28 @@
from __future__ import annotations
import json
import logging
from pathlib import Path
from typing import Dict, Type
from game.factions.faction import Faction
FACTION_DIRECTORY = Path("./resources/factions/")
class FactionLoader:
@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

View File

@@ -1,44 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
France_1995 = {
"country": "France",
"side": "blue",
"units": [
M_2000C,
Mirage_2000_5,
KC_135,
KC130,
C_130,
E_3A,
SA342M,
SA342L,
Armor.MBT_Leclerc,
Armor.TPz_Fuchs, # Standing as VAB
Armor.APC_Cobra, # Standing as VBL
Armor.ATGM_M1134_Stryker, # Standing as VAB Mephisto
Artillery.SPH_M109_Paladin, # Standing as AMX30 AuF1
Artillery.MLRS_M270,
Unarmed.Transport_M818,
Infantry.Infantry_M4,
Infantry.Soldier_M249,
AirDefence.SAM_Roland_ADS,
AirDefence.SAM_Hawk_PCP,
AirDefence.HQ_7_Self_Propelled_LN, # Standing as Crotale
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Armed_speedboat,
], "shorad": [
AirDefence.HQ_7_Self_Propelled_LN,
AirDefence.SAM_Roland_ADS
]
}

View File

@@ -1,59 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
France_2005 = {
"country": "France",
"side": "blue",
"units":[
M_2000C,
Mirage_2000_5,
FA_18C_hornet, # Standing as Rafale M
KC_135,
KC130,
C_130,
E_3A,
SA342M,
SA342L,
Armor.MBT_Leclerc,
Armor.TPz_Fuchs, # Standing as VAB
Armor.APC_Cobra, # Standing as VBL
Armor.ATGM_M1134_Stryker, # Standing as VAB Mephisto
Artillery.SPH_M109_Paladin, # Standing as AMX30 AuF1
Artillery.MLRS_M270,
Unarmed.Transport_M818,
Infantry.Infantry_M4,
Infantry.Soldier_M249,
AirDefence.SAM_Roland_ADS,
AirDefence.SAM_Hawk_PCP,
AirDefence.HQ_7_Self_Propelled_LN, # Standing as Crotale
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Armed_speedboat,
], "shorad":[
AirDefence.HQ_7_Self_Propelled_LN,
AirDefence.SAM_Roland_ADS
], "aircraft_carrier": [
CVN_74_John_C__Stennis, # Standing as CDG Aircraft Carrier
], "helicopter_carrier": [
LHA_1_Tarawa, # Standing as Mistral Class
], "destroyer": [
Oliver_Hazzard_Perry_class,
], "cruiser": [
Ticonderoga_class,
], "carrier_names": [
"R91 Charles de Gaulle",
], "lhanames": [
"L9013 Mistral",
"L9014 Tonerre",
"L9015 Dixmude"
]
}

View File

@@ -1,36 +0,0 @@
from dcs.planes import *
from dcs.vehicles import *
Germany_1944 = {
"country": "Third Reich",
"side": "red",
"units": [
FW_190A8,
FW_190D9,
Bf_109K_4,
Ju_88A4,
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.APC_Sd_Kfz_251,
Armor.IFV_Sd_Kfz_234_2_Puma,
Armor.Sd_Kfz_184_Elefant,
Armor.TD_Jagdpanther_G1,
Armor.TD_Jagdpanzer_IV,
Artillery.Sturmpanzer_IV_Brummbär,
Unarmed.Sd_Kfz_2,
Unarmed.Sd_Kfz_7,
Unarmed.Kübelwagen_82,
Infantry.Infantry_Mauser_98,
AirDefence.AAA_8_8cm_Flak_36,
],
"shorad":[
AirDefence.AAA_8_8cm_Flak_36,
]
}

View File

@@ -1,43 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
Germany_1990 = {
"country": "Germany",
"side": "blue",
"units":[
MiG_29G,
Tornado_IDS,
F_4E,
KC_135,
KC130,
C_130,
E_3A,
UH_1H,
SA342M,
SA342L,
Armor.TPz_Fuchs,
Armor.MBT_Leopard_1A3,
Armor.MBT_Leopard_2,
Armor.IFV_Marder,
Unarmed.Transport_M818,
Infantry.Infantry_M4,
Infantry.Soldier_M249,
AirDefence.SAM_Roland_ADS,
AirDefence.SAM_Hawk_PCP,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Armed_speedboat,
],
"shorad":[
AirDefence.SPAAA_Gepard,
AirDefence.SAM_Roland_ADS,
]
}

View File

@@ -1,53 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
India_2010 = {
"country": "India",
"side": "blue",
"units": [
Mirage_2000_5,
M_2000C,
MiG_27K,
MiG_21Bis,
MiG_29S,
Su_30,
KC_135,
KC130,
C_130,
E_3A,
AH_64A,
Mi_8MT,
Armor.MBT_T_90,
Armor.MBT_T_72B,
Armor.IFV_BMP_2,
Unarmed.Transport_M818,
Infantry.Infantry_M4,
AirDefence.SAM_SA_6_Kub_LN_2P25,
AirDefence.SAM_SA_3_S_125_LN_5P73,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Armed_speedboat,
],
"shorad":[
AirDefence.SAM_SA_8_Osa_9A33,
AirDefence.AAA_ZU_23_Emplacement,
AirDefence.SPAAA_ZSU_23_4_Shilka,
AirDefence.SAM_SA_13_Strela_10M3_9A35M3,
AirDefence.SAM_SA_8_Osa_9A33,
AirDefence.SAM_SA_19_Tunguska_2S6
], "aircraft_carrier": [
CV_1143_5_Admiral_Kuznetsov,
], "destroyer": [
FSG_1241_1MP_Molniya,
], "carrier_names": [
"INS Vikramaditya"
]
}

View File

@@ -1,25 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
Insurgent = {
"country": "Insurgents",
"side": "red",
"units": [
AirDefence.AAA_ZU_23_Insurgent_Closed,
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
Armor.APC_Cobra,
Unarmed.Transport_Ural_375,
Unarmed.Transport_UAZ_469,
Infantry.Soldier_AK,
Infantry.Infantry_Soldier_Insurgents,
Bulk_cargo_ship_Yakushev,
Dry_cargo_ship_Ivanov,
Tanker_Elnya_160
]
}

View File

@@ -1,56 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
Iran_2015 = {
"country": "Iran",
"side": "red",
"units": [
MiG_29A,
F_4E,
F_14B,
F_5E_3,
MiG_21Bis,
Su_24M,
Su_25,
Su_17M4,
IL_76MD,
IL_78M,
An_26B,
An_30M,
Yak_40,
A_50,
Mi_28N,
Mi_24V,
AirDefence.SAM_Hawk_PCP,
AirDefence.SAM_SA_2_LN_SM_90,
AirDefence.SAM_SA_6_Kub_LN_2P25,
AirDefence.HQ_7_Self_Propelled_LN,
AirDefence.SAM_SA_11_Buk_LN_9A310M1,
Armor.APC_M113,
Armor.APC_BTR_80,
Armor.MBT_M60A3_Patton,
Armor.MBT_T_72B,
Unarmed.Transport_Ural_375,
Unarmed.Transport_UAZ_469,
Infantry.Soldier_AK,
CV_1143_5_Admiral_Kuznetsov,
Bulk_cargo_ship_Yakushev,
Dry_cargo_ship_Ivanov,
Tanker_Elnya_160
],
"shorad":[
AirDefence.HQ_7_Self_Propelled_LN,
AirDefence.AAA_ZU_23_Insurgent_Closed
]
}

View File

@@ -1,37 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
Israel_2000 = {
"country": "Israel",
"side": "blue",
"units":[
F_16C_50,
F_15C,
F_4E,
KC_135,
KC130,
C_130,
E_3A,
AH_1W,
AH_64D,
Armor.MBT_Merkava_Mk__4,
Armor.APC_M113,
Unarmed.Transport_M818,
Infantry.Infantry_M4,
AirDefence.SAM_Patriot_EPP_III,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Armed_speedboat,
],
"shorad":[
AirDefence.SAM_Avenger_M1097
]
}

View File

@@ -1,46 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
Italy_1990 = {
"country": "Italy",
"side": "blue",
"units": [
Tornado_IDS,
AV8BNA,
# MB339,
KC_135,
S_3B_Tanker,
C_130,
E_3A,
AH_1W,
UH_1H,
Armor.MBT_Leopard_1A3, # OF-40 MBT
Armor.APC_M113,
Unarmed.Transport_M818,
Infantry.Infantry_M4,
AirDefence.SAM_Hawk_PCP,
AirDefence.SAM_Avenger_M1097,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Armed_speedboat,
], "shorad":[
AirDefence.SAM_Avenger_M1097,
], "helicopter_carrier": [
LHA_1_Tarawa,
], "destroyer": [
Oliver_Hazzard_Perry_class,
], "cruiser": [
Ticonderoga_class,
], "lha_names": [
"Giuseppe Garibaldi",
"Cavour",
]
}

View File

@@ -1,48 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
Lybia_2011 = {
"country": "Russia",
"side": "red",
"units": [
MiG_21Bis,
MiG_23MLD,
Su_24M,
Su_17M4,
Mi_24V,
IL_76MD,
IL_78M,
An_26B,
An_30M,
Yak_40,
A_50,
AirDefence.SAM_SA_8_Osa_9A33,
AirDefence.SAM_SA_2_LN_SM_90,
AirDefence.SAM_SA_3_S_125_LN_5P73,
AirDefence.SAM_SA_6_Kub_LN_2P25,
AirDefence.HQ_7_Self_Propelled_LN,
Armor.IFV_BMP_1,
Armor.FDDM_Grad,
Armor.ARV_BRDM_2,
Armor.MBT_T_55,
Armor.MBT_T_72B,
Artillery.MLRS_BM_21_Grad,
Unarmed.Transport_Ural_375,
Unarmed.Transport_UAZ_469,
Infantry.Paratrooper_RPG_16,
Infantry.Infantry_Soldier_Insurgents
],
"shorad":[
AirDefence.HQ_7_Self_Propelled_LN,
AirDefence.SAM_SA_8_Osa_9A33,
]
}

View File

@@ -1,36 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
Netherlands_1990 = {
"country": "The Netherlands",
"side": "blue",
"units": [
F_16C_50,
F_5E_3,
KC_135,
KC130,
C_130,
E_3A,
AH_64A,
Armor.APC_M113,
Armor.MBT_Leopard_1A3,
Unarmed.Transport_M818,
Infantry.Infantry_M4,
AirDefence.SAM_Hawk_PCP,
AirDefence.SAM_Avenger_M1097,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Armed_speedboat,
],
"shorad":[
AirDefence.SAM_Avenger_M1097
]
}

View File

@@ -1,50 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
NorthKorea_2000 = {
"country": "North Korea",
"side": "red",
"units":[
MiG_29A,
Su_25,
MiG_15bis,
MiG_21Bis,
MiG_23MLD,
MiG_19P,
IL_76MD,
IL_78M,
An_26B,
An_30M,
Yak_40,
A_50,
Mi_8MT,
Mi_24V,
Armor.MBT_T_55,
Armor.MBT_T_72B,
Armor.MBT_T_80U,
Armor.IFV_BMP_1,
Armor.APC_BTR_80,
Armor.ARV_BRDM_2,
Unarmed.Transport_M818,
Infantry.Soldier_AK,
AirDefence.SAM_SA_2_LN_SM_90,
AirDefence.SAM_SA_3_S_125_LN_5P73,
CV_1143_5_Admiral_Kuznetsov,
Bulk_cargo_ship_Yakushev,
Dry_cargo_ship_Ivanov,
Tanker_Elnya_160
],
"shorad":[
AirDefence.AAA_ZU_23_Emplacement,
AirDefence.SPAAA_ZSU_23_4_Shilka
]
}

View File

@@ -1,38 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
Pakistan_2015 = {
"country": "Pakistan",
"side": "blue",
"units": [
JF_17,
F_16C_50,
MiG_21Bis, # Standing as J-7
MiG_19P, # Standing as J-6
IL_78M,
E_3A,
UH_1H,
AH_1W,
Armor.MBT_T_80U,
Armor.MBT_T_55, # Standing as Al-Zarrar / Type 59 MBT
Armor.ZBD_04A,
Armor.APC_BTR_80,
Armor.APC_M113,
Unarmed.Transport_M818,
Infantry.Infantry_M4,
AirDefence.SAM_SA_2_LN_SM_90, # Standing as HQ-2
AirDefence.SAM_SA_10_S_300PS_LN_5P85C, # Standing as HQ-9
Armed_speedboat,
], "shorad": [
AirDefence.HQ_7_Self_Propelled_LN,
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
AirDefence.AAA_ZU_23_Closed
]
}

View File

@@ -1,38 +0,0 @@
from dcs.planes import MiG_15bis, IL_76MD, IL_78M, An_26B, An_30M, Yak_40
from dcs.ships import CV_1143_5_Admiral_Kuznetsov, Bulk_cargo_ship_Yakushev, Dry_cargo_ship_Ivanov, Tanker_Elnya_160
from dcs.vehicles import AirDefence, Armor, Unarmed, Infantry, Artillery
Russia_1955 = {
"country": "Russia",
"side": "red",
"units": [
MiG_15bis,
IL_76MD,
IL_78M,
An_26B,
An_30M,
Yak_40,
AirDefence.AAA_ZU_23_Closed,
AirDefence.AAA_ZU_23_on_Ural_375,
Armor.ARV_BRDM_2,
Armor.FDDM_Grad,
Armor.APC_MTLB,
Armor.MBT_T_55,
Artillery.MLRS_BM_21_Grad,
Unarmed.Transport_Ural_375,
Unarmed.Transport_UAZ_469,
CV_1143_5_Admiral_Kuznetsov,
Bulk_cargo_ship_Yakushev,
Dry_cargo_ship_Ivanov,
Tanker_Elnya_160,
# Infantry squad
Infantry.Paratrooper_AKS,
Infantry.Infantry_Soldier_Rus,
Infantry.Paratrooper_RPG_16,
]
}

View File

@@ -1,53 +0,0 @@
from dcs.helicopters import Mi_8MT
from dcs.planes import MiG_15bis, MiG_19P, MiG_21Bis, IL_76MD, IL_78M, An_26B, An_30M, Yak_40, A_50
from dcs.ships import CV_1143_5_Admiral_Kuznetsov, Bulk_cargo_ship_Yakushev, Dry_cargo_ship_Ivanov, Tanker_Elnya_160
from dcs.vehicles import AirDefence, Armor, Unarmed, Infantry, Artillery
Russia_1965 = {
"country": "Russia",
"side": "red",
"units": [
MiG_15bis,
MiG_19P,
MiG_21Bis,
IL_76MD,
IL_78M,
An_26B,
An_30M,
Yak_40,
A_50,
Mi_8MT,
AirDefence.SAM_SA_6_Kub_LN_2P25,
AirDefence.SAM_SA_2_LN_SM_90,
AirDefence.SAM_SA_3_S_125_LN_5P73,
Armor.ARV_BRDM_2,
Armor.APC_BTR_80,
Armor.ARV_BTR_RD,
Armor.IFV_BMD_1,
Armor.IFV_BMP_1,
Armor.MBT_T_55,
Artillery.MLRS_BM_21_Grad,
Unarmed.Transport_Ural_375,
Unarmed.Transport_UAZ_469,
CV_1143_5_Admiral_Kuznetsov,
Bulk_cargo_ship_Yakushev,
Dry_cargo_ship_Ivanov,
Tanker_Elnya_160,
# Infantry squad
Infantry.Paratrooper_AKS,
Infantry.Infantry_Soldier_Rus,
Infantry.Paratrooper_RPG_16,
],
"shorad":[
AirDefence.AAA_ZU_23_Closed
]
}

View File

@@ -1,69 +0,0 @@
from dcs.helicopters import Mi_8MT, Mi_24V
from dcs.planes import MiG_21Bis, MiG_23MLD, MiG_25PD, MiG_29A, Su_17M4, Su_24M, Su_25, IL_76MD, IL_78M, An_26B, An_30M, \
Yak_40, A_50
from dcs.ships import *
from dcs.vehicles import AirDefence, Armor, Unarmed, Infantry, Artillery
Russia_1975 = {
"country": "Russia",
"side": "red",
"units": [
MiG_21Bis,
MiG_23MLD,
MiG_25PD,
MiG_29A,
Su_17M4,
Su_24M,
Su_25,
IL_76MD,
IL_78M,
An_26B,
An_30M,
Yak_40,
A_50,
Mi_8MT,
Mi_24V,
AirDefence.AAA_ZU_23_Closed,
AirDefence.SAM_SA_6_Kub_LN_2P25,
AirDefence.SAM_SA_3_S_125_LN_5P73,
Armor.ARV_BRDM_2,
Armor.APC_BTR_80,
Armor.IFV_BMD_1,
Armor.IFV_BMP_1,
Armor.MBT_T_55,
Artillery.SPH_2S9_Nona,
Artillery.SPH_2S1_Gvozdika,
Unarmed.Transport_Ural_375,
Unarmed.Transport_UAZ_469,
CV_1143_5_Admiral_Kuznetsov,
Bulk_cargo_ship_Yakushev,
Dry_cargo_ship_Ivanov,
Tanker_Elnya_160,
# Infantry squad
Infantry.Paratrooper_AKS,
Infantry.Infantry_Soldier_Rus,
Infantry.Paratrooper_RPG_16,
],
"shorad": [
AirDefence.AAA_ZU_23_Emplacement,
AirDefence.SPAAA_ZSU_23_4_Shilka
], "aircraft_carrier": [
CV_1143_5_Admiral_Kuznetsov,
], "destroyer": [
FF_1135M_Rezky,
], "cruiser": [
CGN_1144_2_Pyotr_Velikiy,
]
}

View File

@@ -1,68 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
Russia_1990 = {
"country": "Russia",
"side": "red",
"units": [
MiG_23MLD,
MiG_25PD,
MiG_29A,
MiG_29S,
MiG_31,
Su_27,
Su_24M,
Su_25,
IL_76MD,
IL_78M,
An_26B,
An_30M,
Yak_40,
A_50,
Mi_8MT,
Mi_24V,
AirDefence.AAA_ZU_23_Closed,
AirDefence.SAM_SA_6_Kub_LN_2P25,
AirDefence.SAM_SA_3_S_125_LN_5P73,
Armor.ARV_BRDM_2,
Armor.APC_BTR_80,
Armor.IFV_BMD_1,
Armor.IFV_BMP_1,
Armor.MBT_T_55,
Artillery.MLRS_9K57_Uragan_BM_27,
Artillery.SPH_2S19_Msta,
Unarmed.Transport_Ural_375,
Unarmed.Transport_UAZ_469,
CV_1143_5_Admiral_Kuznetsov,
Bulk_cargo_ship_Yakushev,
Dry_cargo_ship_Ivanov,
Tanker_Elnya_160,
# Infantry squad
Infantry.Paratrooper_AKS,
Infantry.Infantry_Soldier_Rus,
Infantry.Paratrooper_RPG_16,
],
"shorad":[
AirDefence.SAM_SA_9_Strela_1_9P31,
AirDefence.SAM_SA_13_Strela_10M3_9A35M3,
AirDefence.SPAAA_ZSU_23_4_Shilka
], "aircraft_carrier": [
CV_1143_5_Admiral_Kuznetsov,
], "destroyer": [
FF_1135M_Rezky,
], "cruiser": [
FSG_1241_1MP_Molniya,
]
}

View File

@@ -1,67 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
Russia_2010 = {
"country": "Russia",
"side": "red",
"units": [
AJS37,
MiG_23MLD,
Su_25,
Su_27,
Su_33,
MiG_29S,
Su_25T,
Su_34,
Su_24M,
L_39ZA,
IL_76MD,
IL_78M,
An_26B,
An_30M,
Yak_40,
A_50,
Ka_50,
Mi_8MT,
AirDefence.SAM_SA_19_Tunguska_2S6,
AirDefence.SAM_SA_11_Buk_LN_9A310M1,
AirDefence.SAM_SA_10_S_300PS_LN_5P85C,
Armor.APC_BTR_80,
Armor.MBT_T_90,
Armor.MBT_T_80U,
Armor.MBT_T_72B,
Artillery.MLRS_9K57_Uragan_BM_27,
Artillery.SPH_2S19_Msta,
Unarmed.Transport_Ural_375,
Unarmed.Transport_UAZ_469,
CV_1143_5_Admiral_Kuznetsov,
Bulk_cargo_ship_Yakushev,
Dry_cargo_ship_Ivanov,
Tanker_Elnya_160,
# Infantry squad
Infantry.Paratrooper_AKS,
Infantry.Infantry_Soldier_Rus,
Infantry.Paratrooper_RPG_16,
],
"shorad":[
AirDefence.SAM_SA_19_Tunguska_2S6,
AirDefence.SAM_SA_13_Strela_10M3_9A35M3
], "aircraft_carrier": [
CV_1143_5_Admiral_Kuznetsov,
], "destroyer": [
FF_1135M_Rezky,
], "cruiser": [
FSG_1241_1MP_Molniya,
]
}

View File

@@ -1,48 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
Spain_1990 = {
"country": "Spain",
"side": "blue",
"units": [
FA_18C_hornet,
AV8BNA,
F_5E_3,
C_101CC,
KC_135,
KC130,
C_130,
E_3A,
Armor.MBT_M60A3_Patton,
Armor.MBT_Leopard_2,
Armor.APC_M113,
Unarmed.Transport_M818,
Infantry.Infantry_M4,
Infantry.Soldier_M249,
AirDefence.SAM_Hawk_PCP,
AirDefence.SAM_Avenger_M1097,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Armed_speedboat,
], "shorad":[
AirDefence.SAM_Avenger_M1097,
], "aircraft_carrier": [
CVN_74_John_C__Stennis, # Standing as Principe de Asturias
], "helicopter_carrier": [
LHA_1_Tarawa, # Standing as Juan Carlos
], "destroyer": [
Oliver_Hazzard_Perry_class,
], "cruiser": [
Ticonderoga_class,
], "carrier_names": [
"Principe de Asturias",
], "lhanames": [
"Juan Carlos I",
]
}

View File

@@ -1,31 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
Sweden_1990 = {
"country": "Sweden",
"side": "blue",
"units": [
AJS37,
UH_1H,
AirDefence.SAM_Hawk_LN_M192,
Armor.IFV_MCV_80, # Standing as Strf 90
Armor.MBT_Leopard_2,
Armor.APC_M1126_Stryker_ICV, # Closest thing available
Unarmed.Transport_Ural_375,
Unarmed.Transport_UAZ_469,
Infantry.Soldier_AK,
CV_1143_5_Admiral_Kuznetsov,
Bulk_cargo_ship_Yakushev,
Dry_cargo_ship_Ivanov,
Tanker_Elnya_160,
],
"shorad":[
AirDefence.SAM_Avenger_M1097
]
}

View File

@@ -1,40 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
Turkey_2005 = {
"country": "Turkey",
"side": "blue",
"units":[
F_16C_50,
F_4E,
KC_135,
KC130,
C_130,
E_3A,
UH_1H,
OH_58D,
AH_1W,
Armor.MBT_Leopard_2,
Armor.MBT_Leopard_1A3,
Armor.MBT_M60A3_Patton,
Armor.APC_Cobra,
Armor.APC_BTR_80,
Unarmed.Transport_M818,
Infantry.Infantry_M4,
AirDefence.SAM_Avenger_M1097,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Armed_speedboat,
], "shorad":[
AirDefence.AAA_ZU_23_Emplacement,
AirDefence.SPAAA_ZSU_23_4_Shilka
]
}

View File

@@ -1,34 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
UAE_2005 = {
"country": "United Arab Emirates",
"side": "blue",
"units":[
M_2000C,
Mirage_2000_5,
F_16C_50,
KC_135,
KC130,
C_130,
E_3A,
AH_64D,
Armor.MBT_Leclerc,
Armor.IFV_BMP_3,
Armor.TPz_Fuchs,
Unarmed.Transport_M818,
Infantry.Infantry_M4,
AirDefence.Rapier_FSA_Launcher,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Armed_speedboat,
]
}

View File

@@ -1,50 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
UnitedKingdom_1990 = {
"country": "UK",
"side": "blue",
"units":[
AV8BNA, # Standing as BAE Harrier 2
Tornado_GR4,
F_4E,
KC_135,
KC130,
C_130,
E_3A,
SA342L,
AH_64A,
Armor.MBT_Challenger_II,
Armor.IFV_MCV_80,
Armor.APC_M1043_HMMWV_Armament,
Armor.ATGM_M1045_HMMWV_TOW,
Unarmed.Transport_M818,
Infantry.Infantry_M4,
Infantry.Soldier_M249,
AirDefence.Rapier_FSA_Launcher,
AirDefence.SAM_Avenger_M1097, # Standing as Starstreak
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Armed_speedboat,
], "shorad":[
AirDefence.SAM_Avenger_M1097,
], "helicopter_carrier": [
LHA_1_Tarawa,
], "destroyer": [
Oliver_Hazzard_Perry_class,
], "cruiser": [
Ticonderoga_class,
], "lha_names": [
"HMS Invincible",
"HMS Illustrious",
"HMS Ark Royal",
]
}

View File

@@ -1,51 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
Ukraine_2010 = {
"country": "Ukraine",
"side": "blue",
"units": [
Su_25,
Su_25T,
Su_24M,
Su_27,
MiG_29S,
L_39ZA,
IL_76MD,
IL_78M,
An_26B,
An_30M,
Yak_40,
A_50,
Mi_8MT,
Mi_24V,
AirDefence.SAM_SA_3_S_125_LN_5P73,
AirDefence.SAM_SA_11_Buk_LN_9A310M1,
AirDefence.SAM_SA_10_S_300PS_LN_5P85C,
Armor.APC_M1043_HMMWV_Armament,
Armor.IFV_BMP_3,
Armor.IFV_BMP_2,
Armor.APC_BTR_80,
Armor.MBT_T_80U,
Armor.MBT_T_72B,
Unarmed.Transport_Ural_375,
Unarmed.Transport_UAZ_469,
Infantry.Soldier_AK,
CV_1143_5_Admiral_Kuznetsov,
Bulk_cargo_ship_Yakushev,
Dry_cargo_ship_Ivanov,
Tanker_Elnya_160,
],
"shorad":[
AirDefence.SAM_SA_19_Tunguska_2S6,
AirDefence.SAM_SA_13_Strela_10M3_9A35M3,
AirDefence.AAA_ZU_23_on_Ural_375
]
}

View File

@@ -1,40 +0,0 @@
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
USA_1944 = {
"country": "USA",
"side": "blue",
"units": [
P_51D,
P_51D_30_NA,
P_47D_30,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
A_20G,
Armor.MT_M4_Sherman,
Armor.MT_M4A4_Sherman_Firefly,
Armor.CT_Cromwell_IV,
Armor.M30_Cargo_Carrier,
Armor.APC_M2A1,
Armor.CT_Cromwell_IV,
Armor.ST_Centaur_IV,
Armor.HIT_Churchill_VII,
Armor.LAC_M8_Greyhound,
Armor.TD_M10_GMC,
Artillery.M12_GMC,
Infantry.Infantry_M1_Garand,
Infantry.Infantry_SMLE_No_4_Mk_1,
LS_Samuel_Chase,
LST_Mk_II,
LCVP__Higgins_boat,
Unarmed.CCKW_353,
AirDefence.AAA_Bofors_40mm,
], "shorad":[
AirDefence.AAA_Bofors_40mm,
]
}

View File

@@ -1,33 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
USA_1955 = {
"country": "USA",
"side": "blue",
"units": [
F_86F_Sabre,
P_51D,
KC_135,
KC130,
C_130,
E_3A,
Armor.MT_M4A4_Sherman_Firefly,
Armor.MT_M4_Sherman,
Armor.MBT_M60A3_Patton,
Armor.APC_M2A1,
Armor.M30_Cargo_Carrier,
Unarmed.Transport_M818,
Infantry.Infantry_M4,
AirDefence.AAA_Bofors_40mm,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Armed_speedboat,
]
}

View File

@@ -1,36 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
USA_1960 = {
"country": "USA",
"side": "blue",
"units": [
F_86F_Sabre,
P_51D,
KC_135,
KC130,
C_130,
E_3A,
UH_1H,
Armor.MBT_M60A3_Patton,
Armor.APC_M113,
Unarmed.Transport_M818,
Infantry.Infantry_M4,
Infantry.Soldier_M249,
AirDefence.AAA_Vulcan_M163,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Armed_speedboat,
],
"shorad":[
AirDefence.AAA_Vulcan_M163
]
}

View File

@@ -1,40 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
USA_1965 = {
"country": "USA",
"side": "blue",
"units": [
F_5E_3,
F_4E,
KC_135,
KC130,
C_130,
E_3A,
B_52H,
UH_1H,
Armor.MBT_M60A3_Patton,
Armor.APC_M113,
Unarmed.Transport_M818,
Infantry.Infantry_M4,
Infantry.Soldier_M249,
AirDefence.SAM_Chaparral_M48,
AirDefence.SAM_Hawk_PCP,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Armed_speedboat,
],
"shorad":[
AirDefence.AAA_Vulcan_M163,
AirDefence.SAM_Chaparral_M48
]
}

View File

@@ -1,63 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
USA_1990 = {
"country": "USA",
"side": "blue",
"units": [
F_15C,
F_14B,
FA_18C_hornet,
A_10A,
AV8BNA,
B_1B,
KC_135,
KC130,
C_130,
E_3A,
UH_1H,
AH_64A,
Armor.MBT_M1A2_Abrams,
Armor.IFV_LAV_25,
Armor.APC_M1043_HMMWV_Armament,
Armor.ATGM_M1045_HMMWV_TOW,
Armor.ATGM_M1134_Stryker,
Unarmed.Transport_M818,
Infantry.Infantry_M4,
Infantry.Soldier_M249,
AirDefence.SAM_Hawk_PCP,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Armed_speedboat,
], "shorad":[
AirDefence.SAM_Avenger_M1097,
], "aircraft_carrier": [
CVN_74_John_C__Stennis,
], "helicopter_carrier": [
LHA_1_Tarawa,
], "destroyer": [
Oliver_Hazzard_Perry_class,
USS_Arleigh_Burke_IIa,
], "cruiser": [
Ticonderoga_class,
], "carrier_names": [
"CVN-72 Abraham Lincoln",
"CVN-73 Georges Washington",
"CVN-74 John C. Stennis",
], "lhanames": [
"LHA-1 Tarawa",
"LHA-2 Saipan",
"LHA-3 Belleau Wood",
"LHA-4 Nassau",
"LHA-5 Peleliu"
]
}

View File

@@ -1,70 +0,0 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
USA_2005 = {
"country": "USA",
"side": "blue",
"units": [
F_15C,
F_14B,
FA_18C_hornet,
F_16C_50,
JF_17,
A_10C,
AV8BNA,
KC_135,
KC130,
C_130,
E_3A,
UH_1H,
AH_64D,
Armor.MBT_M1A2_Abrams,
Armor.ATGM_M1134_Stryker,
Armor.APC_M1126_Stryker_ICV,
Armor.IFV_M2A2_Bradley,
Armor.IFV_LAV_25,
Armor.APC_M1043_HMMWV_Armament,
Armor.ATGM_M1045_HMMWV_TOW,
Artillery.MLRS_M270,
Artillery.SPH_M109_Paladin,
Unarmed.Transport_M818,
Infantry.Infantry_M4,
Infantry.Soldier_M249,
AirDefence.SAM_Hawk_PCP,
AirDefence.SAM_Patriot_EPP_III,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Armed_speedboat,
], "shorad": [
AirDefence.SAM_Avenger_M1097,
], "aircraft_carrier": [
CVN_74_John_C__Stennis,
], "helicopter_carrier": [
LHA_1_Tarawa,
], "destroyer": [
Oliver_Hazzard_Perry_class,
USS_Arleigh_Burke_IIa,
], "cruiser": [
Ticonderoga_class,
], "carrier_names": [
"CVN-71 Theodore Roosevelt",
"CVN-72 Abraham Lincoln",
"CVN-73 George Washington",
"CVN-74 John C. Stennis",
], "lhanames": [
"LHA-1 Tarawa",
"LHA-2 Saipan",
"LHA-3 Belleau Wood",
"LHA-4 Nassau",
"LHA-5 Peleliu"
]
}

View File

@@ -1,11 +1,36 @@
from datetime import datetime, timedelta
import logging
import math
import random
import sys
from datetime import date, datetime, timedelta
from typing import Any, Dict, List
from game.db import REWARDS, PLAYER_BUDGET_BASE
from game.game_stats import GameStats
from gen.flights.ai_flight_planner import FlightPlanner
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 game import db
from game.db import PLAYER_BUDGET_BASE, REWARDS
from game.inventory import GlobalAircraftInventory
from game.models.game_stats import GameStats
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 .event import *
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 plugin import LuaPluginManager
from .weather import Conditions, TimeOfDay
COMMISION_UNIT_VARIETY = 4
COMMISION_LIMITS_SCALE = 1.5
@@ -45,33 +70,66 @@ 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]
turn = 0
game_stats: GameStats = None
current_unit_id = 0
current_group_id = 0
def __init__(self, player_name: str, enemy_name: str, theater: ConflictTheater, start_date: datetime):
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_name = player_name
self.player_country = db.FACTIONS[player_name]["country"]
self.player_country = db.FACTIONS[player_name].country
self.enemy_name = enemy_name
self.enemy_country = db.FACTIONS[enemy_name]["country"]
self.enemy_country = db.FACTIONS[enemy_name].country
self.turn = 0
self.date = datetime(start_date.year, start_date.month, start_date.day)
self.date = date(start_date.year, start_date.month, start_date.day)
self.game_stats = GameStats()
self.game_stats.update(self)
self.planners = {}
self.ground_planners = {}
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):
if self.settings.version == "dev":
@@ -84,10 +142,12 @@ class Game:
self.events.append(event_class(self, player_cp, enemy_cp, enemy_cp.position, self.player_name, self.enemy_name))
def _generate_events(self):
for player_cp, enemy_cp in self.theater.conflicts(True):
self._generate_player_event(FrontlineAttackEvent, player_cp, enemy_cp)
for front_line in self.theater.conflicts(True):
self._generate_player_event(FrontlineAttackEvent,
front_line.control_point_a,
front_line.control_point_b)
def commision_unit_types(self, cp: ControlPoint, for_task: Task) -> typing.Collection[UnitType]:
def commision_unit_types(self, cp: ControlPoint, for_task: Task) -> List[UnitType]:
importance_factor = (cp.importance - IMPORTANCE_LOW) / (IMPORTANCE_HIGH - IMPORTANCE_LOW)
if for_task == AirDefence and not self.settings.sams:
@@ -144,7 +204,7 @@ 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()
@@ -161,26 +221,27 @@ class Game:
def is_player_attack(self, event):
if isinstance(event, Event):
return event.attacker_name == self.player_name
return event and event.attacker_name and event.attacker_name == self.player_name
else:
return event.name == self.player_name
return event and event.name and event.name == self.player_name
def get_player_coalition_id(self):
if self.player_country in db.BLUEFOR_FACTIONS:
return 2
else:
return 1
def on_load(self) -> None:
ObjectiveDistanceCache.set_theater(self.theater)
# set the settings in all plugins
for plugin in LuaPluginManager().getPlugins():
plugin.setSettings(self.settings)
def get_enemy_coalition_id(self):
if self.get_player_coalition_id() == 1:
return 2
else:
return 1
# Save game compatibility.
def pass_turn(self, no_action=False, ignored_cps: typing.Collection[ControlPoint] = None):
# 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 = self.turn + 1
self.turn += 1
for event in self.events:
if self.settings.version == "dev":
@@ -193,7 +254,7 @@ class Game:
self._enemy_reinforcement()
self._budget_player()
if not no_action:
if not no_action and self.turn > 1:
for cp in self.theater.player_points():
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
else:
@@ -201,25 +262,32 @@ class Game:
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()
# Update statistics
self.game_stats.update(self)
# Plan flights & combat for next turn
self.planners = {}
self.ground_planners = {}
self.aircraft_inventory.reset()
for cp in self.theater.controlpoints:
if cp.has_runway():
planner = FlightPlanner(cp, self)
planner.plan_flights()
self.planners[cp.id] = planner
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()
@@ -252,7 +320,7 @@ class Game:
potential_cp_armor = self.theater.enemy_points()
i = 0
potential_units = [u for u in db.FACTIONS[self.enemy_name]["units"] if u in db.UNIT_BY_TASK[PinpointStrike]]
potential_units = db.FACTIONS[self.enemy_name].frontline_units
print("Enemy Recruiting")
print(potential_cp_armor)
@@ -278,8 +346,9 @@ class Game:
if budget_for_armored_units > 0:
budget_for_aircraft += budget_for_armored_units
potential_units = [u for u in db.FACTIONS[self.enemy_name]["units"] if
u in db.UNIT_BY_TASK[CAS] or u in db.UNIT_BY_TASK[CAP]]
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
@@ -297,11 +366,11 @@ class Game:
self.informations.append(info)
@property
def current_turn_daytime(self):
return ["dawn", "day", "dusk", "night"][self.turn % 4]
def current_turn_time_of_day(self) -> TimeOfDay:
return list(TimeOfDay)[self.turn % 4]
@property
def current_day(self):
def current_day(self) -> date:
return self.date + timedelta(days=self.turn // 4)
def next_unit_id(self):
@@ -317,3 +386,92 @@ class Game:
"""
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"

120
game/inventory.py Normal file
View File

@@ -0,0 +1,120 @@
"""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.
"""
return self.inventory[aircraft]
@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 = []

View File

@@ -1,3 +1,5 @@
from typing import List
class FactionTurnMetadata:
"""
Store metadata about a faction
@@ -31,10 +33,8 @@ class GameStats:
Store statistics for the current game
"""
data_per_turn: [GameTurnMetadata] = []
def __init__(self):
self.data_per_turn = []
self.data_per_turn: List[GameTurnMetadata] = []
def update(self, game):
"""

View File

@@ -1,7 +1,8 @@
from game.db import assigned_units_split
from .operation import *
from dcs.terrain.terrain import Terrain
from gen.conflictgen import Conflict
from .operation import Operation
from .. import db
MAX_DISTANCE_BETWEEN_GROUPS = 12000
@@ -14,19 +15,6 @@ class FrontlineAttackOperation(Operation):
attackers = None # type: db.ArmorDict
defenders = None # type: db.ArmorDict
def setup(self,
defenders: db.ArmorDict,
attackers: db.ArmorDict,
strikegroup: db.AssignedUnitsDict,
escort: db.AssignedUnitsDict,
interceptors: db.AssignedUnitsDict):
self.strikegroup = strikegroup
self.escort = escort
self.interceptors = interceptors
self.defenders = defenders
self.attackers = attackers
def prepare(self, terrain: Terrain, is_quick: bool):
super(FrontlineAttackOperation, self).prepare(terrain, is_quick)
if self.defender_name == self.game.player_name:
@@ -47,31 +35,6 @@ class FrontlineAttackOperation(Operation):
conflict=conflict)
def generate(self):
#if self.is_player_attack:
# self.prepare_carriers(db.unitdict_from(self.strikegroup))
# ground units
# self.armorgen.generate_vec(self.attackers, self.defenders)
## strike group w/ heli support
#planes_flights = {k: v for k, v in self.strikegroup.items() if k in plane_map.values()}
#self.airgen.generate_cas_strikegroup(*assigned_units_split(planes_flights), at=self.attackers_starting_position)
#heli_flights = {k: v for k, v in self.strikegroup.items() if k in helicopters.helicopter_map.values()}
#if heli_flights:
# self.briefinggen.append_frequency("FARP + Heli flights", "127.5 MHz AM")
# for farp, dict in zip(self.groundobjectgen.generate_farps(sum([x[0] for x in heli_flights.values()])),
# db.assignedunits_split_to_count(heli_flights, self.groundobjectgen.FARP_CAPACITY)):
# self.airgen.generate_cas_strikegroup(*assigned_units_split(dict),
# at=farp,
# escort=len(planes_flights) == 0)
#self.airgen.generate_attackers_escort(*assigned_units_split(self.escort), at=self.attackers_starting_position)
#self.airgen.generate_defense(*assigned_units_split(self.interceptors), at=self.defenders_starting_position)
self.briefinggen.title = "Frontline CAS"
self.briefinggen.description = "Provide CAS for the ground forces attacking enemy lines. Operation will be considered successful if total number of enemy units will be lower than your own by a factor of 1.5 (i.e. with 12 units from both sides, enemy forces need to be reduced to at least 8), meaning that you (and, probably, your wingmans) should concentrate on destroying the enemy units. Target base strength will be lowered as a result. Be advised that your flight will not attack anything until you explicitly tell them so by comms menu."
self.briefinggen.append_waypoint("CAS AREA IP")
self.briefinggen.append_waypoint("CAS AREA EGRESS")
super(FrontlineAttackOperation, self).generate()

View File

@@ -1,31 +1,56 @@
import logging
import os
from pathlib import Path
from typing import List, Optional, Set
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.terrain import Terrain
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 gen import *
from userdata.debriefing import *
TANKER_CALLSIGNS = ["Texaco", "Arco", "Shell"]
from gen import Conflict, FlightType, VisualGenerator
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
from gen.airfields import AIRFIELD_DATA
from gen.airsupportgen import AirSupport, AirSupportConflictGenerator
from gen.armor import GroundConflictGenerator, JtacInfo
from gen.beacons import load_beacons_for_terrain
from gen.briefinggen import BriefingGenerator
from gen.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 plugin import LuaPluginManager
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
current_mission = None # type: dcs.Mission
regular_mission = None # type: dcs.Mission
quick_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
shipgen = None # type: ShipGenerator
triggersgen = None # type: TriggersGenerator
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
@@ -39,19 +64,20 @@ class Operation:
defender_name: str,
from_cp: ControlPoint,
departure_cp: ControlPoint,
to_cp: ControlPoint = None):
to_cp: ControlPoint):
self.game = game
self.attacker_name = attacker_name
self.attacker_country = db.FACTIONS[attacker_name]["country"]
self.attacker_country = db.FACTIONS[attacker_name].country
self.defender_name = defender_name
self.defender_country = db.FACTIONS[defender_name]["country"]
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) -> typing.Collection[UnitType]:
def units_of(self, country_name: str) -> List[UnitType]:
return []
def is_successfull(self, debriefing: Debriefing) -> bool:
@@ -64,24 +90,14 @@ class Operation:
def initialize(self, mission: Mission, conflict: Conflict):
self.current_mission = mission
self.conflict = conflict
self.airgen = AircraftConflictGenerator(mission, conflict, self.game.settings, self.game)
self.shipgen = ShipGenerator(mission, conflict)
self.airsupportgen = AirSupportConflictGenerator(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)
self.forcedoptionsgen = ForcedOptionsGenerator(mission, conflict, self.game)
self.groundobjectgen = GroundObjectsGenerator(mission, conflict, self.game)
self.briefinggen = BriefingGenerator(mission, conflict, self.game)
player_country = self.from_cp.captured and self.attacker_country or self.defender_country
enemy_country = self.from_cp.captured and self.defender_country or self.attacker_country
self.briefinggen = BriefingGenerator(self.current_mission,
self.conflict, self.game)
def prepare(self, terrain: Terrain, is_quick: bool):
with open("resources/default_options.lua", "r") as f:
options_dict = loads(f.read())["options"]
self.current_mission = dcs.Mission(terrain)
self.current_mission = Mission(terrain)
print(self.game.player_country)
print(country_dict[db.country_id_from_name(self.game.player_country)])
@@ -90,12 +106,12 @@ class Operation:
# Setup coalition :
self.current_mission.coalition["blue"] = Coalition("blue")
self.current_mission.coalition["red"] = Coalition("red")
if self.game.player_country and self.game.player_country in db.BLUEFOR_FACTIONS:
self.current_mission.coalition["blue"].add_country(country_dict[db.country_id_from_name(self.game.player_country)]())
self.current_mission.coalition["red"].add_country(country_dict[db.country_id_from_name(self.game.enemy_country)]())
else:
self.current_mission.coalition["blue"].add_country(country_dict[db.country_id_from_name(self.game.enemy_country)]())
self.current_mission.coalition["red"].add_country(country_dict[db.country_id_from_name(self.game.player_country)]())
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()])
@@ -112,28 +128,134 @@ class Operation:
self.defenders_starting_position = None
else:
self.attackers_starting_position = self.departure_cp.at
self.defenders_starting_position = self.to_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 generate(self):
radio_registry = RadioRegistry()
tacan_registry = TacanRegistry()
# 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)
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.
for frequency in unique_map_frequencies:
radio_registry.reserve(frequency)
# Set mission time and weather conditions.
EnvironmentGenerator(self.current_mission,
self.game.conditions).generate()
# Generate ground object first
self.groundobjectgen.generate()
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
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,
)
# Air Support (Tanker & Awacs)
self.airsupportgen.generate(self.is_awacs_enabled)
airsupportgen = AirSupportConflictGenerator(
self.current_mission, self.conflict, self.game, radio_registry,
tacan_registry)
airsupportgen.generate(self.is_awacs_enabled)
# Generate Activity on the map
for cp in self.game.theater.controlpoints:
side = cp.captured
if side:
country = self.current_mission.country(self.game.player_country)
else:
country = self.current_mission.country(self.game.enemy_country)
if cp.id in self.game.planners.keys():
self.airgen.generate_flights(cp, country, self.game.planners[cp.id])
airgen = AircraftConflictGenerator(
self.current_mission, self.conflict, self.game.settings, self.game,
radio_registry)
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
for player_cp, enemy_cp in self.game.theater.conflicts(True):
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),
@@ -143,6 +265,7 @@ class Operation:
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
@@ -151,57 +274,244 @@ class Operation:
else:
self.current_mission.groundControl.red_tactical_commander = self.ca_slots
# triggers
if self.game.is_player_attack(self.conflict.attackers_country):
cp = self.conflict.from_cp
else:
cp = self.conflict.to_cp
# Triggers
triggersgen = TriggersGenerator(self.current_mission, self.conflict,
self.game)
triggersgen.generate()
self.triggersgen.generate(player_cp=cp,
is_quick=False,
activation_trigger_radius=self.trigger_radius,
awacs_enabled=self.is_awacs_enabled)
# env settings
if self.environment_settings is None:
self.environment_settings = self.envgen.generate()
else:
self.envgen.load(self.environment_settings)
# options
self.forcedoptionsgen.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:
self.visualgen.generate()
visualgen.generate()
# Inject Lua Scripts
load_mist = TriggerStart(comment="Load Mist Lua Framework")
with open("./resources/scripts/mist_4_3_74.lua") as f:
load_mist.add_action(DoScript(String(f.read())))
self.current_mission.triggerrules.triggers.append(load_mist)
luaData = {}
luaData["AircraftCarriers"] = {}
luaData["Tankers"] = {}
luaData["AWACs"] = {}
luaData["JTACs"] = {}
luaData["TargetPoints"] = {}
load_dcs_libe = TriggerStart(comment="Load DCS Liberation Script")
with open("./resources/scripts/dcs_liberation.lua") as f:
script = f.read()
json_location = "[["+os.path.abspath("resources\\scripts\\json.lua")+"]]"
state_location = "[[" + os.path.abspath("state.json") + "]]"
script = script.replace("{{json_file_abs_location}}", json_location)
script = script.replace("{{debriefing_file_location}}", state_location)
load_dcs_libe.add_action(DoScript(String(script)))
self.current_mission.triggerrules.triggers.append(load_dcs_libe)
self.assign_channels_to_flights(airgen.flights,
airsupportgen.air_support)
# Briefing Generation
for i, tanker_type in enumerate(self.airsupportgen.generated_tankers):
self.briefinggen.append_frequency("Tanker {} ({})".format(TANKER_CALLSIGNS[i], tanker_type), "{}X/{} MHz AM".format(97+i, 130+i))
kneeboard_generator = KneeboardGenerator(self.current_mission)
for dynamic_runway in groundobjectgen.runways.values():
self.briefinggen.add_dynamic_runway(dynamic_runway)
for tanker in airsupportgen.air_support.tankers:
self.briefinggen.add_tanker(tanker)
kneeboard_generator.add_tanker(tanker)
luaData["Tankers"][tanker.callsign] = {
"dcsGroupName": tanker.dcsGroupName,
"callsign": tanker.callsign,
"variant": tanker.variant,
"radio": tanker.freq.mhz,
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name
}
if self.is_awacs_enabled:
self.briefinggen.append_frequency("AWACS", "133 MHz AM")
for awacs in airsupportgen.air_support.awacs:
self.briefinggen.add_awacs(awacs)
kneeboard_generator.add_awacs(awacs)
luaData["AWACs"][awacs.callsign] = {
"dcsGroupName": awacs.dcsGroupName,
"callsign": awacs.callsign,
"radio": awacs.freq.mhz
}
self.briefinggen.append_frequency("Flight", "251 MHz AM")
for jtac in jtacs:
self.briefinggen.add_jtac(jtac)
kneeboard_generator.add_jtac(jtac)
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:
self.briefinggen.add_flight(flight)
kneeboard_generator.add_flight(flight)
if flight.friendly and flight.flight_type in [FlightType.ANTISHIP, FlightType.DEAD, FlightType.SEAD, FlightType.STRIKE]:
flightType = flight.flight_type.name
flightTarget = flight.package.target
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}
}
# Generate the briefing
self.briefinggen.generate()
kneeboard_generator.generate()
# set a LUA table with data from Liberation that we want to set
# at the moment it contains Liberation's install path, and an overridable definition for the JTACAutoLase function
# 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().getPlugins():
plugin.injectScripts(self)
plugin.injectConfiguration(self)
self.assign_channels_to_flights(airgen.flights,
airsupportgen.air_support)
kneeboard_generator = KneeboardGenerator(self.current_mission)
for dynamic_runway in groundobjectgen.runways.values():
self.briefinggen.add_dynamic_runway(dynamic_runway)
for tanker in airsupportgen.air_support.tankers:
self.briefinggen.add_tanker(tanker)
kneeboard_generator.add_tanker(tanker)
if self.is_awacs_enabled:
for awacs in airsupportgen.air_support.awacs:
self.briefinggen.add_awacs(awacs)
kneeboard_generator.add_awacs(awacs)
for jtac in jtacs:
self.briefinggen.add_jtac(jtac)
kneeboard_generator.add_jtac(jtac)
for flight in airgen.flights:
self.briefinggen.add_flight(flight)
kneeboard_generator.add_flight(flight)
self.briefinggen.generate()
kneeboard_generator.generate()
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

View File

@@ -1,26 +1,59 @@
from plugin import LuaPluginManager
class Settings:
# Difficulty settings
player_skill = "Good"
enemy_skill = "Average"
enemy_vehicle_skill = "Average"
map_coalition_visibility = "All Units"
labels = "Full"
only_player_takeoff = True # Legacy parameter do not use
night_disabled = False
supercarrier = False
multiplier = 1
sams = True # Legacy parameter do not use
cold_start = False # Legacy parameter do not use
version = None
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
# Performance oriented
perf_red_alert_state = True
perf_smoke_gen = True
perf_artillery = True
perf_moving_units = True
perf_infantry = True
perf_ai_parking_start = True
# 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 = {}
for plugin in LuaPluginManager().getPlugins():
plugin.setSettings(self)
# Cheating
self.show_red_ato = False
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)

2
game/version.py Normal file
View File

@@ -0,0 +1,2 @@
#: Current version of Liberation.
VERSION = "2.2.0-preview"

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,15 +1,13 @@
from .aaa import *
from .aircraft import *
from .armor 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,51 +0,0 @@
from .conflictgen import *
from .naming import *
from dcs.mission import *
from dcs.mission import *
from .conflictgen import *
from .naming import *
DISTANCE_FACTOR = 0.5, 1
EXTRA_AA_MIN_DISTANCE = 50000
EXTRA_AA_MAX_DISTANCE = 150000
EXTRA_AA_POSITION_FROM_CP = 550
class ExtraAAConflictGenerator:
def __init__(self, mission: Mission, conflict: Conflict, game, player_country: Country, enemy_country: Country):
self.mission = mission
self.game = game
self.conflict = conflict
self.player_country = player_country
self.enemy_country = enemy_country
def generate(self):
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
if cp.position.distance_to_point(self.conflict.to_cp.position) < EXTRA_AA_MIN_DISTANCE:
continue
if cp.position.distance_to_point(self.conflict.position) > EXTRA_AA_MAX_DISTANCE:
continue
country_name = cp.captured and self.player_country or self.enemy_country
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_basedefense_name(),
_type=db.EXTRA_AA[country_name],
position=position,
group_size=1
)

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

View File

@@ -1,12 +1,23 @@
from game import db
from .conflictgen import *
from .naming import *
from dataclasses import dataclass, field
from typing import List, Type
from dcs.mission import *
from dcs.unitgroup import *
from dcs.unittype import *
from dcs.task import *
from dcs.terrain.terrain import NoParkingSlotError
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
@@ -16,44 +27,102 @@ AWACS_DISTANCE = 150000
AWACS_ALT = 13000
class AirSupportConflictGenerator:
generated_tankers = None # type: typing.List[str]
@dataclass
class AwacsInfo:
"""AWACS information for the kneeboard."""
dcsGroupName: str
callsign: str
freq: RadioFrequency
def __init__(self, mission: Mission, conflict: Conflict, game):
@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.generated_tankers = []
self.air_support = AirSupport()
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
@classmethod
def support_tasks(cls) -> typing.Collection[typing.Type[MainTask]]:
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)):
self.generated_tankers.append(db.unit_type_name(tanker_unit_type))
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)),
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,
frequency=130 + i,
race_distance=58000,
frequency=freq.mhz,
start_type=StartType.Warm,
tacanchannel="{}X".format(97 + i),
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(ActivateBeaconCommand(channel=97 + i, unit_id=tanker_group.id, aa=False))
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),
@@ -62,11 +131,15 @@ class AirSupportConflictGenerator:
altitude=AWACS_ALT,
airport=None,
position=self.conflict.position.random_point_within(AWACS_DISTANCE, AWACS_DISTANCE),
frequency=133,
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))
except:
print("No AWACS for faction")
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,8 +1,40 @@
from dcs.task import *
import logging
import random
from dataclasses import dataclass
from typing import List
from gen import namegen
from gen.ground_forces.ai_ground_planner import CombatGroupRole, DISTANCE_FROM_FRONTLINE
from .conflictgen 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 plugin import LuaPluginManager
SPREAD_DISTANCE_FACTOR = 0.1, 0.3
SPREAD_DISTANCE_SIZE_FACTOR = 0.1
@@ -17,19 +49,34 @@ 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.m = mission
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.AGGRESIVE, CombatStance.AGGRESIVE, CombatStance.AGGRESIVE, 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.AGGRESIVE])
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]),
)
@@ -58,7 +105,7 @@ class GroundConflictGenerator:
if final_position is not None:
g = self._generate_group(
side=self.m.country(self.game.player_country),
side=self.mission.country(self.game.player_country),
unit=group.units[0],
heading=self.conflict.heading+90,
count=len(group.units),
@@ -66,7 +113,7 @@ class GroundConflictGenerator:
g.set_skill(self.game.settings.player_skill)
player_groups.append((g,group))
self.gen_infantry_group_for_group(g, True, self.m.country(self.game.player_country), self.conflict.heading + 90)
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:
@@ -78,7 +125,7 @@ class GroundConflictGenerator:
if final_position is not None:
g = self._generate_group(
side=self.m.country(self.game.enemy_country),
side=self.mission.country(self.game.enemy_country),
unit=group.units[0],
heading=self.conflict.heading - 90,
count=len(group.units),
@@ -86,14 +133,36 @@ class GroundConflictGenerator:
g.set_skill(self.game.settings.enemy_vehicle_skill)
enemy_groups.append((g, group))
self.gen_infantry_group_for_group(g, False, self.m.country(self.game.enemy_country), self.conflict.heading - 90)
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
jtacPlugin = LuaPluginManager().getPlugin("jtacautolase")
useJTAC = jtacPlugin and jtacPlugin.isEnabled()
if self.game.player_faction.has_jtac and useJTAC:
n = "JTAC" + str(self.conflict.from_cp.id) + str(self.conflict.to_cp.id)
code = 1688 - len(self.jtacs)
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):
@@ -118,7 +187,7 @@ class GroundConflictGenerator:
return
u = random.choice(possible_infantry_units)
self.m.vehicle_group(
self.mission.vehicle_group(
side,
namegen.next_infantry_name(side, cp, u), u,
position=infantry_position,
@@ -126,10 +195,10 @@ class GroundConflictGenerator:
heading=forward_heading,
move_formation=PointAction.OffRoad)
for i in range(randint(3, 10)):
for i in range(random.randint(3, 10)):
u = random.choice(possible_infantry_units)
position = infantry_position.random_point_within(55, 5)
self.m.vehicle_group(
self.mission.vehicle_group(
side,
namegen.next_infantry_name(side, cp, u), u,
position=position,
@@ -144,19 +213,73 @@ class GroundConflictGenerator:
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:
dcs_group.points[0].tasks.append(FireAtPoint(target, len(group.units) * 10, 100))
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.AGGRESIVE:
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(-50, 50), random.randint(-50, 50))
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))
@@ -179,16 +302,20 @@ class GroundConflictGenerator:
targets = self.find_n_nearest_enemy_groups(dcs_group, enemy_groups, 3)
i = 1
for target in targets:
rand_offset = Point(random.randint(-50, 50), random.randint(-50, 50))
dcs_group.add_waypoint(target.points[0].position+rand_offset,PointAction.OffRoad)
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.AGGRESIVE, CombatStance.BREAKTHROUGH, CombatStance.ELIMINATION]:
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)
@@ -196,6 +323,9 @@ class GroundConflictGenerator:
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
@@ -208,14 +338,51 @@ class GroundConflictGenerator:
dcs_group.add_waypoint(reposition_point, PointAction.OffRoad)
def find_retreat_point(self, dcs_group, frontline_heading):
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, RETREAT_DISTANCE)
return dcs_group.points[0].position.point_from_heading(frontline_heading-180, distance)
def find_offensive_point(self, dcs_group, frontline_heading, distance):
"""
@@ -310,7 +477,7 @@ class GroundConflictGenerator:
cp = self.conflict.to_cp
logging.info("armorgen: {} for {}".format(unit, side.id))
group = self.m.vehicle_group(
group = self.mission.vehicle_group(
side,
namegen.next_unit_name(side, cp.id, unit), unit,
position=self._group_point(at),

144
gen/ato.py Normal file
View File

@@ -0,0 +1,144 @@
"""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 typing import Dict, List, Optional
from dcs.mapping import Point
from theater.missiontarget import MissionTarget
from .flights.flight import Flight, FlightType
@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 measured in seconds from mission start.
time_over_target: int = field(default=0)
waypoints: Optional[PackageWaypoints] = field(default=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()

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)

View File

@@ -1,86 +1,251 @@
import logging
import datetime
import os
import random
from collections import defaultdict
from dataclasses import dataclass
from typing import List
from dcs.mission import Mission
from game import db
from .conflictgen import *
from .naming import *
from dcs.mission import *
from .aircraft import FlightData
from .airsupportgen import AwacsInfo, TankerInfo
from .armor import JtacInfo
from .conflictgen import Conflict
from .ground_forces.combat_stance import CombatStance
from .radios import RadioFrequency
from .runways import RunwayData
class BriefingGenerator:
freqs = None # type: typing.List[typing.Tuple[str, str]]
title = "" # type: str
description = "" # type: str
targets = None # type: typing.List[typing.Tuple[str, str]]
waypoints = None # type: typing.List[str]
@dataclass
class CommInfo:
"""Communications information for the kneeboard."""
name: str
freq: RadioFrequency
class MissionInfoGenerator:
"""Base type for generators of mission information for the player.
Examples of subtypes include briefing generators, kneeboard generators, etc.
"""
def __init__(self, mission: Mission) -> None:
self.mission = mission
self.awacs: List[AwacsInfo] = []
self.comms: List[CommInfo] = []
self.flights: List[FlightData] = []
self.jtacs: List[JtacInfo] = []
self.tankers: List[TankerInfo] = []
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 generate(self) -> None:
"""Generates the mission information."""
raise NotImplementedError
class BriefingGenerator(MissionInfoGenerator):
def __init__(self, mission: Mission, conflict: Conflict, game):
self.m = mission
super().__init__(mission)
self.conflict = conflict
self.game = game
self.title = ""
self.description = ""
self.dynamic_runways: List[RunwayData] = []
self.freqs = []
self.targets = []
self.waypoints = []
def add_dynamic_runway(self, runway: RunwayData) -> None:
"""Adds a dynamically generated runway to the briefing.
def append_frequency(self, name: str, frequency: str):
self.freqs.append((name, frequency))
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 append_target(self, description: str, markpoint: str = None):
self.targets.append((description, markpoint))
def append_waypoint(self, description: str):
self.waypoints.append(description)
def add_flight_description(self, flight):
if flight.client_count <= 0:
return
flight_unit_name = db.unit_type_name(flight.unit_type)
self.description += 2 * "\n" + "-" * 50 + "\n"
self.description += flight_unit_name + " x " + str(flight.count) + 2 * "\n"
self.description += "#0 -- TAKEOFF : Take off\n"
for i, wpt in enumerate(flight.points):
self.description += "#" + str(1+i) + " -- " + wpt.name + " : " + wpt.description + "\n"
self.description += "#" + str(len(flight.points) + 1) + " -- RTB\n"
def add_flight_description(self, flight: FlightData):
assert flight.client_units
aircraft = flight.aircraft_type
flight_unit_name = db.unit_type_name(aircraft)
self.description += "-" * 50 + "\n"
self.description += f"{flight_unit_name} x {flight.size}\n\n"
for i, wpt in enumerate(flight.waypoints):
self.description += f"#{i + 1} -- {wpt.name} : {wpt.description}\n"
self.description += f"#{len(flight.waypoints) + 1} -- RTB\n\n"
def add_ally_flight_description(self, flight: FlightData):
assert not flight.client_units
aircraft = flight.aircraft_type
flight_unit_name = db.unit_type_name(aircraft)
delay = datetime.timedelta(seconds=flight.departure_delay)
self.description += (
f"{flight.flight_type.name} {flight_unit_name} x {flight.size}, "
f"departing in {delay}\n"
)
def generate(self):
self.description = ""
self.description += "DCS Liberation turn #" + str(self.game.turn) + "\n"
self.description += "-"*50 + "\n"
self.description += "=" * 15 + "\n\n"
for planner in self.game.planners.values():
for flight in planner.flights:
self.description += (
"Most briefing information, including communications and flight "
"plan information, can be found on your kneeboard.\n\n"
)
self.generate_ongoing_war_text()
self.description += "\n"*2
self.description += "Your flights:" + "\n"
self.description += "=" * 15 + "\n\n"
for flight in self.flights:
if flight.client_units:
self.add_flight_description(flight)
if self.freqs:
self.description += "\n"*2
self.description += "Planned ally flights:" + "\n"
self.description += "=" * 15 + "\n"
allied_flights_by_departure = defaultdict(list)
for flight in self.flights:
if not flight.client_units and flight.friendly:
name = flight.departure.airfield_name
allied_flights_by_departure[name].append(flight)
for departure, flights in allied_flights_by_departure.items():
self.description += f"\nFrom {departure}\n"
self.description += "-" * 50 + "\n\n"
for flight in flights:
self.add_ally_flight_description(flight)
if self.comms:
self.description += "\n\nComms Frequencies:\n"
self.description += "=" * 15 + "\n"
for comm_info in self.comms:
self.description += f"{comm_info.name}: {comm_info.freq}\n"
self.description += ("-" * 50) + "\n"
for runway in self.dynamic_runways:
self.description += f"{runway.airfield_name}\n"
self.description += f"RADIO : {runway.atc}\n"
if runway.tacan is not None:
self.description += f"TACAN : {runway.tacan} {runway.tacan_callsign}\n"
if runway.icls is not None:
self.description += f"ICLS Channel : {runway.icls}\n"
self.description += "-" * 50 + "\n"
for name, freq in self.freqs:
self.description += "\n{}: {}".format(name, freq)
self.description += "\n" + ("-" * 50) + "\n"
for cp in self.game.theater.controlpoints:
if cp.captured and cp.cptype in [ControlPointType.LHA_GROUP, ControlPointType.AIRCRAFT_CARRIER_GROUP]:
self.description += cp.name + " TACAN : "
self.description += str(cp.tacanN)
if cp.tacanY:
self.description += "Y"
else:
self.description += "X"
self.description += " " + str(cp.tacanI) + "\n"
if cp.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP and hasattr(cp, "icls"):
self.description += "ICLS Channel : " + str(cp.icls) + "\n"
self.description += "-" * 50 + "\n"
self.m.set_description_text(self.description)
self.description += "JTACS [F-10 Menu] : \n"
self.description += "===================\n\n"
for jtac in self.jtacs:
self.description += f"{jtac.region} -- Code : {jtac.code}\n"
self.mission.set_description_text(self.description)
self.mission.add_picture_blue(os.path.abspath(
"./resources/ui/splash_screen.png"))
def generate_ongoing_war_text(self):
self.description += "Current situation:\n"
self.description += "=" * 15 + "\n\n"
conflict_number = 0
for front_line in self.game.theater.conflicts(from_player=True):
conflict_number = conflict_number + 1
player_base = front_line.control_point_a
enemy_base = front_line.control_point_b
has_numerical_superiority = player_base.base.total_armor > enemy_base.base.total_armor
self.description += self.__random_frontline_sentence(player_base.name, enemy_base.name)
if enemy_base.id in player_base.stances.keys():
stance = player_base.stances[enemy_base.id]
if player_base.base.total_armor == 0:
self.description += "We do not have a single vehicle available to hold our position, the situation is critical, and we will lose ground inevitably.\n"
elif enemy_base.base.total_armor == 0:
self.description += "The enemy forces have been crushed, we will be able to make significant progress toward " + enemy_base.name + ". \n"
if stance == CombatStance.AGGRESSIVE:
if has_numerical_superiority:
self.description += "On this location, our ground forces will try to make progress against the enemy"
self.description += ". As the enemy is outnumbered, our forces should have no issue making progress.\n"
else:
self.description += "On this location, our ground forces will try an audacious assault against enemies in superior numbers. The operation is risky, and the enemy might counter attack.\n"
elif stance == CombatStance.ELIMINATION:
if has_numerical_superiority:
self.description += "On this location, our ground forces will focus on the destruction of enemy assets, before attempting to make progress toward " + enemy_base.name + ". "
self.description += "The enemy is already outnumbered, and this maneuver might draw a final blow to their forces.\n"
else:
self.description += "On this location, our ground forces will try an audacious assault against enemies in superior numbers. The operation is risky, and the enemy might counter attack.\n"
elif stance == CombatStance.BREAKTHROUGH:
if has_numerical_superiority:
self.description += "On this location, our ground forces will focus on progression toward " + enemy_base.name + ".\n"
else:
self.description += "On this location, our ground forces have been ordered to rush toward " + enemy_base.name + ". Wish them luck... We are also expecting a counter attack.\n"
elif stance in [CombatStance.DEFENSIVE, CombatStance.AMBUSH]:
if has_numerical_superiority:
self.description += "On this location, our ground forces will hold position. We are not expecting an enemy assault.\n"
else:
self.description += "On this location, our ground forces have been ordered to hold still, and defend against enemy attacks. An enemy assault might be iminent.\n"
if conflict_number == 0:
self.description += "There are currently no fights on the ground.\n"
self.description += "\n\n"
def __random_frontline_sentence(self, player_base_name, enemy_base_name):
templates = [
"There are combats between {} and {}. ",
"The war on the ground is still going on between {} and {}. ",
"Our ground forces in {} are opposed to enemy forces based in {}. ",
"Our forces from {} are fighting enemies based in {}. ",
"There is an active frontline between {} and {}. ",
]
return random.choice(templates).format(player_base_name, enemy_base_name)

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,21 +1,11 @@
import logging
import typing
import pdb
import dcs
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
@@ -65,24 +55,6 @@ def _heading_sum(h, a) -> int:
class Conflict:
attackers_side = None # type: str
defenders_side = None # type: str
attackers_country = None # type: Country
defenders_country = 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]
heading = None # type: int
distance = None # type: 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,
theater: ConflictTheater,
from_cp: ControlPoint,
@@ -155,7 +127,7 @@ class Conflict:
else:
return self.position
def find_ground_position(self, at: Point, heading: int, max_distance: int = 40000) -> typing.Optional[Point]:
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
@@ -163,7 +135,7 @@ class Conflict:
return from_cp.has_frontline and to_cp.has_frontline
@classmethod
def frontline_position(cls, theater: ConflictTheater, from_cp: ControlPoint, to_cp: ControlPoint) -> typing.Optional[typing.Tuple[Point, int]]:
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)
@@ -174,9 +146,7 @@ class Conflict:
@classmethod
def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> typing.Optional[typing.Tuple[Point, int, int]]:
initial, heading = cls.frontline_position(theater, from_cp, to_cp)
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) ])
@@ -193,9 +163,6 @@ class Conflict:
return Point(*intersection.xy[0]), _heading_sum(heading, 90), intersection.length
"""
frontline = cls.frontline_position(theater, from_cp, to_cp)
if not frontline:
return None
center_position, heading = frontline
left_position, right_position = None, None
@@ -243,7 +210,7 @@ class Conflict:
"""
@classmethod
def _find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> typing.Optional[Point]:
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):
@@ -302,10 +269,14 @@ class Conflict:
distance = to_cp.size * GROUND_DISTANCE_FACTOR
attackers_location = position.point_from_heading(attack_heading, distance)
attackers_location = Conflict._find_ground_position(attackers_location, distance * 2, _heading_sum(attack_heading, 180), theater)
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, 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,
@@ -429,7 +400,7 @@ class Conflict:
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, randint(0, int(distance)))
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))
@@ -456,7 +427,9 @@ class Conflict:
distance = to_cp.size * GROUND_DISTANCE_FACTOR
defenders_location = position.point_from_heading(defense_heading, distance)
defenders_location = Conflict._find_ground_position(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,

View File

@@ -3,22 +3,37 @@ import random
from dcs.vehicles import Armor
from game import db
from gen.defenses.armored_group_generator import ArmoredGroupGenerator
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
:param parentCp: The parent control point
:param ground_object: The ground object which will own the group
:param country: Owner country
:return: Generated group
"""
possible_unit = [u for u in db.FACTIONS[faction]["units"] if u in Armor.__dict__.values()]
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)
generator = ArmoredGroupGenerator(game, ground_object, unit_type)
generator.generate()
return generator.get_generated_group()
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

@@ -25,3 +25,20 @@ class ArmoredGroupGenerator(GroupGenerator):
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,156 +1,36 @@
import logging
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 = 1600
WEATHER_FOG_CHANCE = 20
WEATHER_FOG_VISIBILITY = 2500, 5000
WEATHER_FOG_THICKNESS = 100, 500
RANDOM_TIME = {
"night": 7,
"dusk": 40,
"dawn": 40,
"day": 100,
}
RANDOM_WEATHER = {
1: 0, # thunderstorm
2: 20, # rain
3: 80, # clouds
4: 100, # clear
}
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_time(self):
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
start_time = self.game.current_day
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
daytime = self.game.current_turn_daytime
logging.info("Mission time will be {}".format(daytime))
if self.game.settings.night_disabled:
logging.info("Skip Night mission due to user settings")
if daytime == "dawn":
time_range = (8, 9)
elif daytime == "day":
time_range = (10, 12)
elif daytime == "dusk":
time_range = (12, 14)
elif daytime == "night":
time_range = (14, 17)
else:
time_range = (10, 12)
else:
time_range = self.game.theater.daytime_map[daytime]
start_time += timedelta(hours=random.randint(*time_range))
logging.info("time - {}, slot - {}, night skipped - {}".format(
str(start_time),
str(time_range),
self.game.settings.night_disabled))
self.mission.start_time = start_time
def _generate_wind(self, wind_speed, wind_direction=None):
# wind
if not wind_direction:
wind_direction = random.randint(0, 360)
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)
def _generate_base_weather(self):
# clouds
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
self._generate_wind(random.randint(0, 4))
# fog
if random.randint(0, 100) < WEATHER_FOG_CHANCE:
self.mission.weather.fog_visibility = random.randint(*WEATHER_FOG_VISIBILITY)
self.mission.weather.fog_thickness = random.randint(*WEATHER_FOG_THICKNESS)
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
logging.info("generated weather {}".format(weather_type))
if weather_type == 1:
# thunderstorm
self._generate_base_weather()
self._generate_wind(random.randint(8, 12))
self.mission.weather.clouds_density = random.randint(9, 10)
self.mission.weather.clouds_iprecptns = Weather.Preceptions.Thunderstorm
elif weather_type == 2:
# rain
self._generate_base_weather()
self.mission.weather.clouds_density = random.randint(5, 8)
self.mission.weather.clouds_iprecptns = Weather.Preceptions.Rain
self._generate_wind(random.randint(4, 8))
elif weather_type == 3:
# clouds
self._generate_base_weather()
elif weather_type == 4:
# clear
pass
if self.mission.weather.clouds_density > 0:
# sometimes clouds are randomized way too low and need to be fixed
self.mission.weather.clouds_base = max(self.mission.weather.clouds_base, WEATHER_CLOUD_BASE_MIN)
if self.mission.weather.wind_at_ground.speed == 0:
# frontline smokes look silly w/o any wind
self._generate_wind(1)
def generate(self) -> EnvironmentSettings:
self._gen_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

@@ -12,22 +12,19 @@ class CarrierGroupGenerator(GroupGenerator):
def generate(self):
# Add carrier
if "aircraft_carrier" in self.faction.keys():
if "supercarrier" in self.faction.keys() and self.game.settings.supercarrier:
carrier_type = random.choice(self.faction["supercarrier"])
else:
carrier_type = random.choice(self.faction["aircraft_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
dd_type = random.choice(self.faction["destroyer"])
self.add_unit(dd_type, "DD1", self.position.x + 250, self.position.y + 450, self.heading)
self.add_unit(dd_type, "DD2", self.position.x + 250, self.position.y - 450, self.heading)
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 + 450, self.position.y + 850, self.heading)
self.add_unit(dd_type, "DD4", self.position.x + 450, self.position.y - 850, 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

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

@@ -0,0 +1,42 @@
import random
from gen.fleet.dd_group import DDGroupGenerator
from gen.sam.group_generator import GroupGenerator
from dcs.ships import *
class ChineseNavyGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
super(ChineseNavyGroupGenerator, self).__init__(game, ground_object)
self.faction = faction
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, ground_object, faction):
super(Type54GroupGenerator, self).__init__(game, ground_object, faction, Type_054A_Frigate)

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

@@ -0,0 +1,27 @@
import random
from gen.sam.group_generator import GroupGenerator
from dcs.ships import *
class DDGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction, ddtype):
super(DDGroupGenerator, self).__init__(game, ground_object)
self.faction = 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, ground_object, faction):
super(OliverHazardPerryGroupGenerator, self).__init__(game, ground_object, faction, Oliver_Hazzard_Perry_class)
class ArleighBurkeGroupGenerator(DDGroupGenerator):
def __init__(self, game, ground_object, faction):
super(ArleighBurkeGroupGenerator, self).__init__(game, ground_object, faction, USS_Arleigh_Burke_IIa)

View File

@@ -12,14 +12,14 @@ class LHAGroupGenerator(GroupGenerator):
def generate(self):
# Add carrier
if "helicopter_carrier" in self.faction.keys():
carrier_type = random.choice(self.faction["helicopter_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 "destroyer" in self.faction.keys():
dd_type = random.choice(self.faction["destroyer"])
self.add_unit(dd_type, "DD1", self.position.x + 250, self.position.y + 450, self.heading)
self.add_unit(dd_type, "DD2", self.position.x + 250, self.position.y - 450, self.heading)
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

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

@@ -0,0 +1,59 @@
import random
from gen.fleet.dd_group import DDGroupGenerator
from gen.sam.group_generator import GroupGenerator
from dcs.ships import *
class RussianNavyGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
super(RussianNavyGroupGenerator, self).__init__(game, ground_object)
self.faction = faction
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, ground_object, faction):
super(GrishaGroupGenerator, self).__init__(game, ground_object, faction, FFL_1124_4_Grisha)
class MolniyaGroupGenerator(DDGroupGenerator):
def __init__(self, game, ground_object, faction):
super(MolniyaGroupGenerator, self).__init__(game, ground_object, faction, FSG_1241_1MP_Molniya)
class KiloSubGroupGenerator(DDGroupGenerator):
def __init__(self, game, ground_object, faction):
super(KiloSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_877)
class TangoSubGroupGenerator(DDGroupGenerator):
def __init__(self, game, ground_object, faction):
super(TangoSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_641B)

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

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

View File

@@ -1,6 +1,48 @@
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):

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

@@ -0,0 +1,19 @@
import random
from dcs.ships import Uboat_VIIC_U_flak
from gen.sam.group_generator import GroupGenerator
class UBoatGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
super(UBoatGroupGenerator, self).__init__(game, ground_object)
self.faction = faction
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

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

@@ -0,0 +1,22 @@
import random
from dcs.ships import LS_Samuel_Chase, LST_Mk_II
from gen.sam.group_generator import GroupGenerator
class WW2LSTGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
super(WW2LSTGroupGenerator, self).__init__(game, ground_object)
self.faction = faction
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

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,108 @@
from dcs.planes import *
from dcs.helicopters import *
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_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_14B,
F_15C,
@@ -18,19 +111,23 @@ INTERCEPT_CAPABLE = [
# 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,
@@ -40,6 +137,8 @@ CAP_CAPABLE = [
F_5E_3,
F_14B,
F_15C,
F_15E,
F_16A,
F_16C_50,
FA_18C_hornet,
@@ -49,6 +148,8 @@ CAP_CAPABLE = [
P_51D_30_NA,
P_51D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
@@ -56,9 +157,49 @@ CAP_CAPABLE = [
Bf_109K_4,
FW_190D9,
FW_190A8,
A_4E_C,
Rafale_M,
]
# USed for CAS (Close air support) and BAI (Battlefield Interdiction)
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_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,
@@ -80,23 +221,34 @@ CAS_CAPABLE = [
A_10A,
A_10C,
A_10C_2,
AV8BNA,
F_86F_Sabre,
F_5E_3,
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,
@@ -108,6 +260,8 @@ CAS_CAPABLE = [
P_51D_30_NA,
P_51D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
A_20G,
SpitfireLFMkIXCW,
@@ -116,13 +270,74 @@ CAS_CAPABLE = [
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_16C_50, Not yet
F_15E,
F_16C_50,
AV8BNA,
JF_17,
@@ -133,14 +348,25 @@ SEAD_CAPABLE = [
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_29A,
MiG_27K,
MiG_29S,
MB_339PAN,
Su_17M4,
Su_24M,
@@ -149,20 +375,34 @@ STRIKE_CAPABLE = [
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_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,
@@ -170,7 +410,10 @@ STRIKE_CAPABLE = [
P_51D_30_NA,
P_51D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
A_20G,
B_17G,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
@@ -179,17 +422,47 @@ STRIKE_CAPABLE = [
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]

View File

@@ -1,14 +1,21 @@
from enum import Enum
from typing import List
from __future__ import annotations
from dcs.mission import StartType
from enum import Enum
from typing import Dict, Iterable, List, Optional, TYPE_CHECKING
from dcs.mapping import Point
from dcs.point import MovingPoint, PointAction
from dcs.unittype import UnitType
from game import db
from theater.controlpoint import ControlPoint, MissionTarget
if TYPE_CHECKING:
from gen.ato import Package
class FlightType(Enum):
CAP = 0
CAP = 0 # Do not use. Use BARCAP or TARCAP.
TARCAP = 1
BARCAP = 2
CAS = 3
@@ -46,61 +53,123 @@ class FlightWaypointType(Enum):
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
class PredefinedWaypointCategory(Enum):
NOT_PREDEFINED = 0
ALLY_CP = 1
ENEMY_CP = 2
FRONTLINE = 3
ENEMY_BUILDING = 4
ENEMY_UNIT = 5
ALLY_BUILDING = 6
ALLY_UNIT = 7
class FlightWaypoint:
def __init__(self, x: float, y: float, alt=0):
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 = []
self.targets: List[MissionTarget] = []
self.targetGroup: Optional[MissionTarget] = None
self.obj_name = ""
self.pretty_name = ""
self.waypoint_type = FlightWaypointType.TAKEOFF # type: FlightWaypointType
self.category: PredefinedWaypointCategory = PredefinedWaypointCategory.NOT_PREDEFINED
self.only_for_player = False
self.data = None
# These are set very late by the air conflict generator (part of mission
# generation). We do it late so that we don't need to propagate changes
# to waypoint times whenever the player alters the package TOT or the
# flight's offset in the UI.
self.tot: Optional[int] = None
self.departure_time: Optional[int] = None
@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:
unit_type: UnitType = None
from_cp = None
points: List[FlightWaypoint] = []
flight_type: FlightType = None
count: int = 0
client_count: int = 0
targets = []
use_custom_loadout = False
loadout = {}
preset_loadout_name = ""
start_type = "Runway"
group = False # Contains DCS Mission group data after mission has been generated
# How long before this flight should take off
scheduled_in = 0
def __init__(self, unit_type: UnitType, count: int, from_cp, flight_type: FlightType):
def __init__(self, package: Package, unit_type: UnitType, 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
self.points = []
self.targets = []
self.loadout = {}
self.start_type = "Runway"
self.points: List[FlightWaypoint] = []
self.targets: List[MissionTarget] = []
self.loadout: Dict[str, str] = {}
self.start_type = start_type
# Late activation delay in seconds from mission start. This is not
# the same as the flight's takeoff time. Takeoff time depends on the
# mission's TOT and the other flights in the package. Takeoff time is
# determined by AirConflictGenerator.
self.scheduled_in = 0
def __repr__(self):
return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \
+ " in " + str(self.scheduled_in) + " minutes (" + str(len(self.points)) + " wpt)"
+ " (" + str(len(self.points)) + " wpt)"
# Test
if __name__ == '__main__':
from dcs.planes import A_10C
from theater import ControlPoint, Point, List
from_cp = ControlPoint(0, "AA", Point(0, 0), None, [], 0, 0)
f = Flight(A_10C, 4, from_cp, FlightType.CAS)
f.scheduled_in = 50
print(f)
def waypoint_with_type(
self,
types: Iterable[FlightWaypointType]) -> Optional[FlightWaypoint]:
for waypoint in self.points:
if waypoint.waypoint_type in types:
return waypoint
return None

454
gen/flights/flightplan.py Normal file
View File

@@ -0,0 +1,454 @@
"""Flight plan generation.
Flights are first planned generically by either the player or by the
MissionPlanner. Those only plan basic information like the objective, aircraft
type, and the size of the flight. The FlightPlanBuilder is responsible for
generating the waypoints for the mission.
"""
from __future__ import annotations
import logging
import random
from typing import List, Optional, TYPE_CHECKING
from dcs.mapping import Point
from dcs.unit import Unit
from game.data.doctrine import Doctrine, MODERN_DOCTRINE
from game.utils import nm_to_meter
from gen.ato import Package, PackageWaypoints
from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject
from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint
from .waypointbuilder import WaypointBuilder
from ..conflictgen import Conflict
if TYPE_CHECKING:
from game import Game
class InvalidObjectiveLocation(RuntimeError):
"""Raised when the objective location is invalid for the mission type."""
def __init__(self, task: FlightType, location: MissionTarget) -> None:
super().__init__(
f"{location.name} is not valid for {task.name} missions."
)
class FlightPlanBuilder:
"""Generates flight plans for flights."""
def __init__(self, game: Game, package: Package, is_player: bool) -> None:
self.game = game
self.package = package
self.is_player = is_player
if is_player:
faction = self.game.player_faction
else:
faction = self.game.enemy_faction
self.doctrine: Doctrine = faction.doctrine
def populate_flight_plan(
self, flight: Flight,
# TODO: Custom targets should be an attribute of the flight.
custom_targets: Optional[List[Unit]] = None) -> None:
"""Creates a default flight plan for the given mission."""
if flight not in self.package.flights:
raise RuntimeError("Flight must be a part of the package")
if self.package.waypoints is None:
self.regenerate_package_waypoints()
# TODO: Flesh out mission types.
try:
task = flight.flight_type
if task == FlightType.ANTISHIP:
logging.error(
"Anti-ship flight plan generation not implemented"
)
elif task == FlightType.BAI:
logging.error("BAI flight plan generation not implemented")
elif task == FlightType.BARCAP:
self.generate_barcap(flight)
elif task == FlightType.CAS:
self.generate_cas(flight)
elif task == FlightType.DEAD:
self.generate_sead(flight, custom_targets)
elif task == FlightType.ELINT:
logging.error("ELINT flight plan generation not implemented")
elif task == FlightType.ESCORT:
self.generate_escort(flight)
elif task == FlightType.EVAC:
logging.error("Evac flight plan generation not implemented")
elif task == FlightType.EWAR:
logging.error("EWar flight plan generation not implemented")
elif task == FlightType.INTERCEPTION:
logging.error(
"Intercept flight plan generation not implemented"
)
elif task == FlightType.LOGISTICS:
logging.error(
"Logistics flight plan generation not implemented"
)
elif task == FlightType.RECON:
logging.error("Recon flight plan generation not implemented")
elif task == FlightType.SEAD:
self.generate_sead(flight, custom_targets)
elif task == FlightType.STRIKE:
self.generate_strike(flight)
elif task == FlightType.TARCAP:
self.generate_frontline_cap(flight)
elif task == FlightType.TROOP_TRANSPORT:
logging.error(
"Troop transport flight plan generation not implemented"
)
else:
logging.error(f"Unsupported task type: {task.name}")
except InvalidObjectiveLocation:
logging.exception(f"Could not create flight plan")
def regenerate_package_waypoints(self) -> None:
ingress_point = self._ingress_point()
egress_point = self._egress_point()
join_point = self._join_point(ingress_point)
split_point = self._split_point(egress_point)
self.package.waypoints = PackageWaypoints(
join_point,
ingress_point,
egress_point,
split_point,
)
def generate_strike(self, flight: Flight) -> None:
"""Generates a strike flight plan.
Args:
flight: The flight to generate the flight plan for.
"""
assert self.package.waypoints is not None
location = self.package.target
# TODO: Support airfield strikes.
if not isinstance(location, TheaterGroundObject):
raise InvalidObjectiveLocation(flight.flight_type, location)
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join)
builder.ingress_strike(self.package.waypoints.ingress, location)
if len(location.groups) > 0 and location.dcs_identifier == "AA":
# TODO: Replace with DEAD?
# Strike missions on SEAD targets target units.
for g in location.groups:
for j, u in enumerate(g.units):
builder.strike_point(u, f"{u.type} #{j}", location)
else:
# TODO: Does this actually happen?
# ConflictTheater is built with the belief that multiple ground
# objects have the same name. If that's the case,
# TheaterGroundObject needs some refactoring because it behaves very
# differently for SAM sites than it does for strike targets.
buildings = self.game.theater.find_ground_objects_by_obj_name(
location.obj_name
)
for building in buildings:
if building.is_dead:
continue
builder.strike_point(building, building.category, location)
builder.egress(self.package.waypoints.egress, location)
builder.split(self.package.waypoints.split)
builder.rtb(flight.from_cp)
flight.points = builder.build()
def generate_barcap(self, flight: Flight) -> None:
"""Generate a BARCAP flight at a given location.
Args:
flight: The flight to generate the flight plan for.
"""
location = self.package.target
if isinstance(location, FrontLine):
raise InvalidObjectiveLocation(flight.flight_type, location)
patrol_alt = random.randint(
self.doctrine.min_patrol_altitude,
self.doctrine.max_patrol_altitude
)
closest_cache = ObjectiveDistanceCache.get_closest_airfields(location)
for airfield in closest_cache.closest_airfields:
# If the mission is a BARCAP of an enemy airfield, find the *next*
# closest enemy airfield.
if airfield == self.package.target:
continue
if airfield.captured != self.is_player:
closest_airfield = airfield
break
else:
logging.error("Could not find any enemy airfields")
return
heading = location.position.heading_between_point(
closest_airfield.position
)
min_distance_from_enemy = nm_to_meter(20)
distance_to_airfield = int(closest_airfield.position.distance_to_point(
self.package.target.position
))
distance_to_no_fly = distance_to_airfield - min_distance_from_enemy
min_cap_distance = min(self.doctrine.cap_min_distance_from_cp,
distance_to_no_fly)
max_cap_distance = min(self.doctrine.cap_max_distance_from_cp,
distance_to_no_fly)
end = location.position.point_from_heading(
heading,
random.randint(min_cap_distance, max_cap_distance)
)
diameter = random.randint(
self.doctrine.cap_min_track_length,
self.doctrine.cap_max_track_length
)
start = end.point_from_heading(heading - 180, diameter)
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(flight.from_cp)
builder.race_track(start, end, patrol_alt)
builder.rtb(flight.from_cp)
flight.points = builder.build()
def generate_frontline_cap(self, flight: Flight) -> None:
"""Generate a CAP flight plan for the given front line.
Args:
flight: The flight to generate the flight plan for.
"""
assert self.package.waypoints is not None
location = self.package.target
if not isinstance(location, FrontLine):
raise InvalidObjectiveLocation(flight.flight_type, location)
ally_cp, enemy_cp = location.control_points
patrol_alt = random.randint(self.doctrine.min_patrol_altitude,
self.doctrine.max_patrol_altitude)
# Find targets waypoints
ingress, heading, distance = Conflict.frontline_vector(
ally_cp, enemy_cp, self.game.theater
)
center = ingress.point_from_heading(heading, distance / 2)
orbit_center = center.point_from_heading(
heading - 90, random.randint(nm_to_meter(6), nm_to_meter(15))
)
combat_width = distance / 2
if combat_width > 500000:
combat_width = 500000
if combat_width < 35000:
combat_width = 35000
radius = combat_width*1.25
orbit0p = orbit_center.point_from_heading(heading, radius)
orbit1p = orbit_center.point_from_heading(heading + 180, radius)
# Create points
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join)
builder.race_track(orbit0p, orbit1p, patrol_alt)
builder.split(self.package.waypoints.split)
builder.rtb(flight.from_cp)
flight.points = builder.build()
def generate_sead(self, flight: Flight,
custom_targets: Optional[List[Unit]]) -> None:
"""Generate a SEAD/DEAD flight at a given location.
Args:
flight: The flight to generate the flight plan for.
custom_targets: Specific radar equipped units selected by the user.
"""
assert self.package.waypoints is not None
location = self.package.target
if not isinstance(location, TheaterGroundObject):
raise InvalidObjectiveLocation(flight.flight_type, location)
if custom_targets is None:
custom_targets = []
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join)
builder.ingress_sead(self.package.waypoints.ingress, location)
# TODO: Unify these.
# There doesn't seem to be any reason to treat the UI fragged missions
# different from the automatic missions.
if custom_targets:
for target in custom_targets:
if flight.flight_type == FlightType.DEAD:
builder.dead_point(target, location.name, location)
else:
builder.sead_point(target, location.name, location)
else:
if flight.flight_type == FlightType.DEAD:
builder.dead_area(location)
else:
builder.sead_area(location)
builder.egress(self.package.waypoints.egress, location)
builder.split(self.package.waypoints.split)
builder.rtb(flight.from_cp)
flight.points = builder.build()
def _hold_point(self, flight: Flight) -> Point:
heading = flight.from_cp.position.heading_between_point(
self.package.target.position
)
return flight.from_cp.position.point_from_heading(
heading, nm_to_meter(15)
)
def generate_escort(self, flight: Flight) -> None:
assert self.package.waypoints is not None
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join)
builder.escort(self.package.waypoints.ingress,
self.package.target, self.package.waypoints.egress)
builder.split(self.package.waypoints.split)
builder.rtb(flight.from_cp)
flight.points = builder.build()
def generate_cas(self, flight: Flight) -> None:
"""Generate a CAS flight plan for the given target.
Args:
flight: The flight to generate the flight plan for.
"""
assert self.package.waypoints is not None
location = self.package.target
if not isinstance(location, FrontLine):
raise InvalidObjectiveLocation(flight.flight_type, location)
ingress, heading, distance = Conflict.frontline_vector(
location.control_points[0], location.control_points[1],
self.game.theater
)
center = ingress.point_from_heading(heading, distance / 2)
egress = ingress.point_from_heading(heading, distance)
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join)
builder.ingress_cas(ingress, location)
builder.cas(center)
builder.egress(egress, location)
builder.split(self.package.waypoints.split)
builder.rtb(flight.from_cp)
flight.points = builder.build()
# TODO: Make a model for the waypoint builder and use that in the UI.
def generate_ascend_point(self, flight: Flight,
departure: ControlPoint) -> FlightWaypoint:
"""Generate ascend point.
Args:
flight: The flight to generate the descend point for.
departure: Departure airfield or carrier.
"""
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(departure)
return builder.build()[0]
def generate_descend_point(self, flight: Flight,
arrival: ControlPoint) -> FlightWaypoint:
"""Generate approach/descend point.
Args:
flight: The flight to generate the descend point for.
arrival: Arrival airfield or carrier.
"""
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.descent(arrival)
return builder.build()[0]
def generate_rtb_waypoint(self, flight: Flight,
arrival: ControlPoint) -> FlightWaypoint:
"""Generate RTB landing point.
Args:
flight: The flight to generate the landing waypoint for.
arrival: Arrival airfield or carrier.
"""
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.land(arrival)
return builder.build()[0]
def _join_point(self, ingress_point: Point) -> Point:
heading = self._heading_to_package_airfield(ingress_point)
return ingress_point.point_from_heading(heading,
-self.doctrine.join_distance)
def _split_point(self, egress_point: Point) -> Point:
heading = self._heading_to_package_airfield(egress_point)
return egress_point.point_from_heading(heading,
-self.doctrine.split_distance)
def _ingress_point(self) -> Point:
heading = self._target_heading_to_package_airfield()
return self.package.target.position.point_from_heading(
heading - 180 + 25, self.doctrine.ingress_egress_distance
)
def _egress_point(self) -> Point:
heading = self._target_heading_to_package_airfield()
return self.package.target.position.point_from_heading(
heading - 180 - 25, self.doctrine.ingress_egress_distance
)
def _target_heading_to_package_airfield(self) -> int:
return self._heading_to_package_airfield(self.package.target.position)
def _heading_to_package_airfield(self, point: Point) -> int:
return self.package_airfield().position.heading_between_point(point)
def package_airfield(self) -> ControlPoint:
# We'll always have a package, but if this is being planned via the UI
# it could be the first flight in the package.
if not self.package.flights:
raise RuntimeError(
"Cannot determine source airfield for package with no flights"
)
# The package airfield is either the flight's airfield (when there is no
# package) or the closest airfield to the objective that is the
# departure airfield for some flight in the package.
cache = ObjectiveDistanceCache.get_closest_airfields(
self.package.target
)
for airfield in cache.closest_airfields:
for flight in self.package.flights:
if flight.from_cp == airfield:
return airfield
raise RuntimeError(
"Could not find any airfield assigned to this package"
)

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

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

View File

@@ -0,0 +1,348 @@
from __future__ import annotations
from typing import List, Optional, Union
from dcs.mapping import Point
from dcs.unit import Unit
from game.data.doctrine import Doctrine
from game.utils import nm_to_meter
from game.weather import Conditions
from theater import ControlPoint, MissionTarget, TheaterGroundObject
from .flight import Flight, FlightWaypoint, FlightWaypointType
from ..runways import RunwayAssigner
class WaypointBuilder:
def __init__(self, conditions: Conditions, flight: Flight,
doctrine: Doctrine) -> None:
self.conditions = conditions
self.flight = flight
self.doctrine = doctrine
self.waypoints: List[FlightWaypoint] = []
self.ingress_point: Optional[FlightWaypoint] = None
@property
def is_helo(self) -> bool:
return getattr(self.flight.unit_type, "helicopter", False)
def build(self) -> List[FlightWaypoint]:
return self.waypoints
def ascent(self, departure: ControlPoint) -> None:
"""Create ascent waypoint for the given departure airfield or carrier.
Args:
departure: Departure airfield or carrier.
"""
heading = RunwayAssigner(self.conditions).takeoff_heading(departure)
position = departure.position.point_from_heading(
heading, nm_to_meter(5)
)
waypoint = FlightWaypoint(
FlightWaypointType.ASCEND_POINT,
position.x,
position.y,
500 if self.is_helo else self.doctrine.pattern_altitude
)
waypoint.name = "ASCEND"
waypoint.alt_type = "RADIO"
waypoint.description = "Ascend"
waypoint.pretty_name = "Ascend"
self.waypoints.append(waypoint)
def descent(self, arrival: ControlPoint) -> None:
"""Create descent waypoint for the given arrival airfield or carrier.
Args:
arrival: Arrival airfield or carrier.
"""
landing_heading = RunwayAssigner(self.conditions).landing_heading(
arrival)
heading = (landing_heading + 180) % 360
position = arrival.position.point_from_heading(
heading, nm_to_meter(5)
)
waypoint = FlightWaypoint(
FlightWaypointType.DESCENT_POINT,
position.x,
position.y,
300 if self.is_helo else self.doctrine.pattern_altitude
)
waypoint.name = "DESCEND"
waypoint.alt_type = "RADIO"
waypoint.description = "Descend to pattern altitude"
waypoint.pretty_name = "Descend"
self.waypoints.append(waypoint)
def land(self, arrival: ControlPoint) -> None:
"""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"
self.waypoints.append(waypoint)
def hold(self, position: Point) -> None:
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"
self.waypoints.append(waypoint)
def join(self, position: Point) -> None:
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"
self.waypoints.append(waypoint)
def split(self, position: Point) -> None:
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"
self.waypoints.append(waypoint)
def ingress_cas(self, position: Point, objective: MissionTarget) -> None:
self._ingress(FlightWaypointType.INGRESS_CAS, position, objective)
def ingress_escort(self, position: Point, objective: MissionTarget) -> None:
self._ingress(FlightWaypointType.INGRESS_ESCORT, position, objective)
def ingress_sead(self, position: Point, objective: MissionTarget) -> None:
self._ingress(FlightWaypointType.INGRESS_SEAD, position, objective)
def ingress_strike(self, position: Point, objective: MissionTarget) -> None:
self._ingress(FlightWaypointType.INGRESS_STRIKE, position, objective)
def _ingress(self, ingress_type: FlightWaypointType, position: Point,
objective: MissionTarget) -> None:
if self.ingress_point is not None:
raise RuntimeError("A flight plan can have only one ingress point.")
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"
self.waypoints.append(waypoint)
self.ingress_point = waypoint
def egress(self, position: Point, target: MissionTarget) -> None:
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"
self.waypoints.append(waypoint)
def dead_point(self, target: Union[TheaterGroundObject, Unit], name: str,
location: MissionTarget) -> None:
self._target_point(target, name, f"STRIKE {name}", location)
# TODO: Seems fishy.
if self.ingress_point is not None:
self.ingress_point.targetGroup = location
def sead_point(self, target: Union[TheaterGroundObject, Unit], name: str,
location: MissionTarget) -> None:
self._target_point(target, name, f"STRIKE {name}", location)
# TODO: Seems fishy.
if self.ingress_point is not None:
self.ingress_point.targetGroup = location
def strike_point(self, target: Union[TheaterGroundObject, Unit], name: str,
location: MissionTarget) -> None:
self._target_point(target, name, f"STRIKE {name}", location)
def _target_point(self, target: Union[TheaterGroundObject, Unit], name: str,
description: str, location: MissionTarget) -> None:
if self.ingress_point is None:
raise RuntimeError(
"An ingress point must be added before target points."
)
waypoint = FlightWaypoint(
FlightWaypointType.TARGET_POINT,
target.position.x,
target.position.y,
0
)
waypoint.description = description
waypoint.pretty_name = description
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
self.waypoints.append(waypoint)
# TODO: This seems wrong, but it's what was there before.
self.ingress_point.targets.append(location)
def sead_area(self, target: MissionTarget) -> None:
self._target_area(f"SEAD on {target.name}", target)
# TODO: Seems fishy.
if self.ingress_point is not None:
self.ingress_point.targetGroup = target
def dead_area(self, target: MissionTarget) -> None:
self._target_area(f"DEAD on {target.name}", target)
# TODO: Seems fishy.
if self.ingress_point is not None:
self.ingress_point.targetGroup = target
def _target_area(self, name: str, location: MissionTarget) -> None:
if self.ingress_point is None:
raise RuntimeError(
"An ingress point must be added before target points."
)
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
self.waypoints.append(waypoint)
# TODO: This seems wrong, but it's what was there before.
self.ingress_point.targets.append(location)
def cas(self, position: Point) -> None:
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"
self.waypoints.append(waypoint)
def race_track_start(self, position: Point, altitude: int) -> None:
"""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"
self.waypoints.append(waypoint)
def race_track_end(self, position: Point, altitude: int) -> None:
"""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"
self.waypoints.append(waypoint)
def race_track(self, start: Point, end: Point, altitude: int) -> None:
"""Creates two waypoint for a racetrack orbit.
Args:
start: The beginning racetrack waypoint.
end: The ending racetrack waypoint.
altitude: The racetrack altitude.
"""
self.race_track_start(start, altitude)
self.race_track_end(end, altitude)
def rtb(self, arrival: ControlPoint) -> None:
"""Creates descent ant landing waypoints for the given control point.
Args:
arrival: Arrival airfield or carrier.
"""
self.descent(arrival)
self.land(arrival)
def escort(self, ingress: Point, target: MissionTarget,
egress: Point) -> None:
"""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.
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"
self.waypoints.append(waypoint)
self.egress(egress, target)

View File

@@ -22,15 +22,22 @@ class ForcedOptionsGenerator:
self.game = game
def _set_options_view(self):
if self.game.settings.map_coalition_visibility == "All Units":
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 == "Allied Units":
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 == "Own Aircraft":
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 == "None":
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)
@@ -41,5 +48,8 @@ class ForcedOptionsGenerator:
def generate(self):
self._set_options_view()
self._set_external_views()
self._set_labels()

View File

@@ -1,9 +1,11 @@
import random
from enum import Enum
from typing import Dict, List
from dcs.vehicles import *
from dcs.vehicles import Armor, Artillery, Infantry, Unarmed
from dcs.unittype import VehicleType
from gen import Conflict
import pydcs_extensions.frenchpack.frenchpack as frenchpack
from gen.ground_forces.combat_stance import CombatStance
from theater import ControlPoint
@@ -32,6 +34,18 @@ TYPE_TANKS = [
Armor.ST_Centaur_IV,
Armor.CT_Cromwell_IV,
Armor.HIT_Churchill_VII,
# 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 = [
@@ -44,6 +58,12 @@ TYPE_ATGM = [
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 = [
@@ -61,6 +81,12 @@ TYPE_IFV = [
# WW2
Armor.IFV_Sd_Kfz_234_2_Puma,
Armor.LAC_M8_Greyhound,
# Mods
frenchpack.ERC_90,
frenchpack.VBAE_CRAB,
frenchpack.VAB_T20_13
]
TYPE_APC = [
@@ -81,6 +107,12 @@ TYPE_APC = [
# WW2
Armor.APC_M2A1,
Armor.APC_Sd_Kfz_251,
# Mods
frenchpack.VAB__50,
frenchpack.VBL__50,
frenchpack.VBL_AANF1,
]
TYPE_ARTILLERY = [
@@ -117,6 +149,11 @@ TYPE_LOGI = [
Unarmed.Willys_MB,
Unarmed.Land_Rover_109_S3,
Unarmed.Land_Rover_101_FC,
# Mods
frenchpack.VBL,
frenchpack.VAB,
]
TYPE_INFANTRY = [
@@ -148,19 +185,19 @@ class CombatGroupRole(Enum):
DISTANCE_FROM_FRONTLINE = {
CombatGroupRole.TANK:2800,
CombatGroupRole.APC:7000,
CombatGroupRole.IFV:3000,
CombatGroupRole.ARTILLERY:14000,
CombatGroupRole.SHORAD:12000,
CombatGroupRole.LOGI:18000,
CombatGroupRole.INFANTRY:2800,
CombatGroupRole.ATGM:5500
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.AGGRESIVE: [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],
@@ -170,8 +207,8 @@ GROUP_SIZES_BY_COMBAT_STANCE = {
class CombatGroup:
def __init__(self, role:CombatGroupRole):
self.units = []
def __init__(self, role: CombatGroupRole):
self.units: List[VehicleType] = []
self.role = role
self.assigned_enemy_cp = None
self.start_position = None
@@ -185,33 +222,22 @@ class CombatGroup:
class GroundPlanner:
cp = None
combat_groups_dict = {}
connected_enemy_cp = []
tank_groups = []
apc_group = []
ifv_group = []
art_group = []
shorad_groups = []
logi_groups = []
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 = []
self.apc_group = []
self.ifv_group = []
self.art_group = []
self.atgm_group = []
self.logi_groups = []
self.shorad_groups = []
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 = {}
self.units_per_cp: Dict[int, List[CombatGroup]] = {}
for cp in self.connected_enemy_cp:
self.units_per_cp[cp.id] = []
self.reserve = []
self.reserve: List[CombatGroup] = []
def plan_groundwar(self):

View File

@@ -3,7 +3,7 @@ from enum import Enum
class CombatStance(Enum):
DEFENSIVE = 0 # Unit will adopt defensive stance with medium group of units
AGGRESIVE = 1 # Unit will attempt to make progress with medium sized 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

View File

@@ -1,26 +1,342 @@
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 .conflictgen import *
from .naming import *
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
from dcs.mission import *
from dcs.statics import *
FARP_FRONTLINE_DISTANCE = 10000
AA_CP_MIN_DISTANCE = 40000
class GenericGroundObjectGenerator:
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:
# Only covers SAMs and missile sites now.
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):
def __init__(self, ground_object: BuildingGroundObject, country: Country,
game: Game, mission: Mission) -> None:
super().__init__(ground_object, country, game, mission)
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.string_identifier,
_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.string_identifier,
_type=static_type,
position=self.ground_object.position,
heading=self.ground_object.heading,
dead=self.ground_object.is_dead,
)
class GenericCarrierGenerator(GenericGroundObjectGenerator):
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):
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):
def tacan_callsign(self) -> str:
# TODO: Assign these properly.
return random.choice([
"LHD",
"LHA",
"LHB",
"LHC",
"LHD",
"LDS",
])
class ShipObjectGenerator(GenericGroundObjectGenerator):
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:
FARP_CAPACITY = 4
def __init__(self, mission: Mission, conflict: Conflict, game):
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) -> typing.Collection[StaticGroup]:
def generate_farps(self, number_of_units=1) -> Iterator[StaticGroup]:
if self.conflict.is_vector:
center = self.conflict.center
heading = self.conflict.heading - 90
@@ -43,122 +359,34 @@ class GroundObjectsGenerator:
)
def generate(self):
cp = None # type: ControlPoint
if self.conflict.attackers_country.name == self.game.player_country:
cp = self.conflict.to_cp
else:
cp = self.conflict.from_cp
consumed_farps = set()
for cp in self.game.theater.controlpoints:
if cp.captured:
country = self.game.player_country
country_name = self.game.player_country
else:
country = self.game.enemy_country
side = self.m.country(country)
country_name = self.game.enemy_country
country = self.m.country(country_name)
for ground_object in cp.ground_objects:
if ground_object.dcs_identifier == "AA":
for g in ground_object.groups:
if len(g.units) > 0:
utype = unit_type_from_name(g.units[0].type)
vg = self.m.vehicle_group(side, g.name, utype, position=g.position, heading=g.units[0].heading)
vg.units[0].name = self.m.string(g.units[0].name)
for i, u in enumerate(g.units):
if i > 0:
vehicle = Vehicle(self.m.next_unit_id(), self.m.string(u.name), u.type)
vehicle.position.x = u.position.x
vehicle.position.y = u.position.y
vehicle.heading = u.heading
vg.add_unit(vehicle)
if self.game.settings.perf_red_alert_state:
vg.points[0].tasks.append(OptAlarmState(2))
else:
vg.points[0].tasks.append(OptAlarmState(1))
elif ground_object.dcs_identifier in ["CARRIER", "LHA"]:
for g in ground_object.groups:
if len(g.units) > 0:
utype = unit_type_from_name(g.units[0].type)
if ground_object.dcs_identifier == "CARRIER" and self.game.settings.supercarrier == True:
utype = db.upgrade_to_supercarrier(utype, cp.name)
sg = self.m.ship_group(side, g.name, utype, position=g.position, heading=g.units[0].heading)
sg.units[0].name = self.m.string(g.units[0].name)
for i, u in enumerate(g.units):
if i > 0:
ship = Ship(self.m.next_unit_id(), self.m.string(u.name), unit_type_from_name(u.type))
ship.position.x = u.position.x
ship.position.y = u.position.y
ship.heading = u.heading
sg.add_unit(ship)
# TODO : make sure the point is not on Land
sg.add_waypoint(sg.points[0].position.point_from_heading(g.units[0].heading, 100000))
# SET UP TACAN
modeChannel = "X" if not cp.tacanY else "Y"
sg.points[0].tasks.append(ActivateBeaconCommand(channel=cp.tacanN, modechannel=modeChannel, callsign=cp.tacanI, unit_id=sg.units[0].id))
if ground_object.dcs_identifier == "CARRIER" and hasattr(cp, "icls"):
sg.points[0].tasks.append(ActivateICLSCommand(cp.icls, unit_id=sg.units[0].id))
if isinstance(ground_object, BuildingGroundObject):
generator = BuildingSiteGenerator(ground_object, country,
self.game, self.m)
elif isinstance(ground_object, CarrierGroundObject):
generator = CarrierGenerator(ground_object, cp, country,
self.game, self.m,
self.radio_registry,
self.tacan_registry,
self.icls_alloc, self.runways)
elif isinstance(ground_object, LhaGroundObject):
generator = CarrierGenerator(ground_object, cp, country,
self.game, self.m,
self.radio_registry,
self.tacan_registry,
self.icls_alloc, self.runways)
elif isinstance(ground_object, ShipGroundObject):
generator = ShipObjectGenerator(ground_object, country,
self.game, self.m)
else:
if ground_object.dcs_identifier in warehouse_map:
static_type = warehouse_map[ground_object.dcs_identifier]
else:
static_type = fortification_map[ground_object.dcs_identifier]
if not static_type:
print("Didn't find {} in static _map(s)!".format(ground_object.dcs_identifier))
continue
group = self.m.static_group(
country=side,
name=ground_object.string_identifier,
_type=static_type,
position=ground_object.position,
heading=ground_object.heading,
dead=ground_object.is_dead,
)
logging.info("generated {}object identifier {} with mission id {}".format("dead " if ground_object.is_dead else "", group.name, group.id))
def farp_aa(mission_obj, country, name, position: mapping.Point):
"""
Add AAA to a FARP :)
:param mission_obj:
:param country:
:param name:
:param position:
:return:
"""
vg = unitgroup.VehicleGroup(mission_obj.next_group_id(), mission_obj.string(name))
units = [
AirDefence.SPAAA_ZSU_23_4_Shilka,
AirDefence.AAA_ZU_23_Closed,
]
v = mission_obj.vehicle(name + "_AAA", random.choice(units))
v.position.x = position.x - random.randint(5, 30)
v.position.y = position.y - random.randint(5, 30)
v.heading = random.randint(0, 359)
vg.add_unit(v)
wp = vg.add_waypoint(vg.units[0].position, PointAction.OffRoad, 0)
wp.ETA_locked = True
country.add_vehicle_group(vg)
return vg
generator = GenericGroundObjectGenerator(ground_object,
country, self.game,
self.m)
generator.generate()

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