Compare commits

...

222 Commits

Author SHA1 Message Date
Dan Albert
61879aeafa Fix line endings.
These get broken whenever someone uses the GitHub file upload editor,
since that doesn't understand .gitattributes.

(cherry picked from commit 6f4ac1dc39)
2023-05-23 00:50:38 -07:00
Starfire13
f5b9052257 Update Golan Heights and Caen to Evreux campaigns.
I had asked Khopa for permission to edit two of his campaigns to fix
some issues. Only the YAMLs have been edited, .miz files did not need
changes. I have tested both YAMLs to make sure campaigns will generate.
Also tested generating turn 1 .miz and ran it in DCS.

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

Caen to Evreux:
1. Re-arranged squadrons for better force distribution and revised
primary and secondary mission types for better default play experience.
2. Implemented squadron limits.

(cherry picked from commit f831c8efdd)
2023-05-23 00:50:38 -07:00
Starfire13
b27d2be0d1 Update Apache loadouts.
BAI loadout updated to use the new radar guided hellfires. Aux tanks
removed in favour of extra cannon ammo.

(cherry picked from commit e3c6b03603)
2023-05-20 02:34:02 -07:00
Dan Albert
e1a1eca5da Fix syntax error in bluefor_modern.yaml.
(cherry picked from commit 7a2e8279cd)
2023-05-19 18:00:32 -07:00
Dan Albert
c695db0f98 Checkpoint game before sim, auto-revert on abort.
An alternative to
https://github.com/dcs-liberation/dcs_liberation/pull/2891 that I ended
up liking much better (I had assumed some part of the UI would fail or
at least look terrible with this approach, but it seems to work quite
well).

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

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

(cherry picked from commit 24e72475b4)
2023-05-19 17:53:34 -07:00
Starfire13
ff20f16109 Update scenic_inland.yaml
A formatting fix for scenic route 2 that was preventing new campaign start. Fixing at Fuzzle's request as he doesn't have the time for it right now.

(cherry picked from commit f10350dac4)
2023-05-19 17:43:59 -07:00
Dan Albert
8af3dc6965 Fuzzle campaign updates.
https://github.com/dcs-liberation/dcs_liberation/issues/2889
(cherry picked from commit f068976749)
2023-05-19 01:28:08 -07:00
Dan Albert
e6cf253e45 Attempt to reset the simulation on abort.
This is optional because I really don't know if I trust it. I don't see
much wrong with it (aside from the warning about not using it with auto-
resolve, because it won't restore lost aircraft), but it's really not
something I'd built for since it's not going to be possible as the RTS
features grow.

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

(cherry picked from commit 4b4c45e90f)
2023-05-19 01:19:49 -07:00
Dan Albert
8dc3fca290 Update contributors list.
<3
2023-05-18 22:24:00 -07:00
dependabot[bot]
0d18b57074 Bump starlette from 0.26.1 to 0.27.0
Includes dependent update to fastapi.

Bumps [starlette](https://github.com/encode/starlette) from 0.26.1 to 0.27.0.
- [Release notes](https://github.com/encode/starlette/releases)
- [Changelog](https://github.com/encode/starlette/blob/master/docs/release-notes.md)
- [Commits](https://github.com/encode/starlette/compare/0.26.1...0.27.0)

---
updated-dependencies:
- dependency-name: starlette
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-18 22:23:08 -07:00
Dan Albert
b745e7c8ec Rehiding speed controls for 7.
7 is shipping sooner than expected, so re-hide these since it's not
ready yet.

https://github.com/dcs-liberation/dcs_liberation/issues/2746
2023-05-18 22:02:37 -07:00
Dan Albert
800ca598ef Add missing changlog note for C-47.
https://github.com/dcs-liberation/dcs_liberation/pull/2812
2023-05-18 21:38:08 -07:00
Dan Albert
212813e31d Update pydcs.
Support for DCS 2.8.5.40170, including laser hellfire, pylon 5 for the
apache, and a more tolerant livery scanner (not all liveries will be
discovered, but Liberation at least won't crash).

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2880.
2023-05-18 21:30:18 -07:00
Dan Albert
571fe21d57 Fix campaign line endings. 2023-05-18 21:16:29 -07:00
Starfire13
cd952312b7 Update Starfire's campaign updates for 10.7. 2023-05-17 19:03:48 -07:00
Dan Albert
23982fdac6 Add changelog note for pydcs livery scanner crash fix.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2747.
2023-05-16 21:28:29 -07:00
Dan Albert
8724b458a8 Add changelog note for TALD overfly fix.
https://github.com/dcs-liberation/dcs_liberation/issues/2781
2023-05-16 20:56:33 -07:00
Dan Albert
ef64899701 Add changelog note for "recreate as" fix.
https://github.com/dcs-liberation/dcs_liberation/issues/2779
2023-05-16 20:56:33 -07:00
Dan Albert
51e4dc5c22 Add changelog note for fixed recovery tanker tasks.
https://github.com/dcs-liberation/dcs_liberation/issues/2771
2023-05-16 20:56:33 -07:00
Dan Albert
ac5edeb936 Add changelog note for improved dead event handling.
https://github.com/dcs-liberation/dcs_liberation/issues/2765
2023-05-16 20:56:33 -07:00
Dan Albert
f7364d04ed Add changelog note for air wing reset fix.
https://github.com/dcs-liberation/dcs_liberation/issues/2751
2023-05-16 20:56:33 -07:00
Dan Albert
483bf73213 Add changelog note for BAI planning fix.
https://github.com/dcs-liberation/dcs_liberation/issues/2618
2023-05-16 20:56:33 -07:00
Dan Albert
0b87f90d4f Add changelog note for built-in TGP fix. 2023-05-16 20:56:33 -07:00
Dan Albert
202fe1109b Add changelog note for AEW&C planning fix.
https://github.com/dcs-liberation/dcs_liberation/issues/2048
2023-05-16 20:56:33 -07:00
Dan Albert
5a8863b07e Add changelog note for TALD fix.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2780.
2023-05-16 20:56:33 -07:00
Dan Albert
889e1f5da2 Finish wiring up SAM orientation control.
The UI works beautifully, but that's not worth much if it doesn't
actually change the data...

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2479.
2023-05-16 18:28:18 -07:00
ColonelAkirNakesh
418d78f99b Adds J-11 flyable, Jeff and BS3 liveries
Adds J-11 to CJTF Blue. Enforces fictional USAF Aggressor livery for J-11, fictional US livery for Jeff, non-Russian 'neutral black' livery for BS3.
2023-05-16 18:10:53 -07:00
ColonelAkirNakesh
a4b5bc198c Restores Flak 18 to free German faction 2023-05-16 18:09:37 -07:00
zhexu14
fcb2f21d36 add unit tests for mission_types function in controlpoint objects (#2870)
This PR adds unit tests that can be planned against control points.
2023-05-16 18:09:03 -07:00
zhexu14
15abf3d6fe Update ADM141 data to include clsids for hornet 2023-05-16 18:06:50 -07:00
Dan Albert
a8b7aca4fb Make wind speed moddable.
These should probably be overridable per theater and per season, but
even with that we'll want some defaults.

https://github.com/dcs-liberation/dcs_liberation/issues/2862
2023-05-16 00:52:51 -07:00
Dan Albert
799dbfa99c Move and split up weather.py.
This is getting out of hand, and I'm about to make it worse.
2023-05-16 00:52:51 -07:00
Dan Albert
eb31a0f038 Rework wind speed Weibull inputs, tune.
The previous method of using a uniform scalar of the MSL wind speed for
higher altitudes didn't offer enough control. In particular, the shape
needs to be quite different to skew low, mid, high.

This patch reworks that system so the parameters of each distribution
are configured per-altitude level. To keep some continuity between
altitudes (on a windy day, all levels should have higher wind speeds on
average), the wind speed of the lower altitude will be added to the
scale value of the higher altitude.

Since it wasn't practical to approximate the previous behavior with the
new system, this also handles the tuning of each. The low altitude
speeds remain mostly unchanged (typically around 5 knots expect for
thunderstorms), but the average speeds for other altitudes went up to
more closely match the previous intent but without the massive
overshoot. At 2000m wind speeds are typically in the 20-25 knot range
now, and 8000m 30-50 knots.

https://www.quora.com/What-is-the-average-wind-speed-at-different-altitudes
has some of the source data, and Quora is the most authoritative source
there is. It claims that cruise altitude winds can get "as high as 150
knots", but doesn't claim anything about the average. I had a
surprisingly difficult time finding good data for cruise altitude air
speeds for non-jet stream paths (though many of our maps are in jet
streams), so I just eyeballed it from
https://turbli.com/wind-during-flights/.

https://github.com/dcs-liberation/dcs_liberation/issues/2861

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2863.
2023-05-15 22:44:17 -07:00
Dan Albert
78e2da9196 Use weibull distribution for wind generation.
Wind speeds should not be uniformly distributed. This switches to a
Weibull distribution which allegedly (see the bug) is good enough.
Experimentally that seems true as well, though I know nothing about how
wind works irl. This at least looks like it'll generate reasonable
variation in missions while keeping the 1st through 3rd quartile
behaviors from getting out of hand.

I'm very uncertain about the scaling factor aspect of this. Naively the
wind speeds at different altitudes ought to be somewhat correlated, but
I'm not sure how much, and whether this kind of scaling is at all the
right way to do it. As before, meh, close enough?

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2861.
2023-05-15 21:37:43 -07:00
Dan Albert
ca96a232f0 Revert "Use the actual Country type instead of the name."
This reverts commit bd2ec12e0f.

Country is both the data (name, ID, etc) and the container for groups
added to the miz, so it can't be used across multiple mission
generations. See https://github.com/pydcs/dcs/issues/314 for potential
follow up work that would let us do this.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2864.
2023-05-15 18:23:48 -07:00
Dan Albert
03671bbfb0 Allow manual SAM orientation.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2479.
2023-05-13 17:20:03 -07:00
Dan Albert
97c4168d13 Wrap settings titles. 2023-05-13 16:59:49 -07:00
Dan Albert
e0edfa68b1 Make settings scrollable. 2023-05-13 16:59:49 -07:00
Dan Albert
97c238a4bb Warn players that take off disallows new flights. 2023-05-13 16:47:56 -07:00
Dan Albert
a6c5b03212 Do not create refueling tasks without tankers.
If the package does not have a tanker, the refueling task will cause AI
flights to go to an arbitrary tanker, which may cause them to fly
through enemy territory or even go farther than their arrival airbase.

It's also not remotely possible for every AI flight in the game to
refuel in most missions. There's typically one tanker and dozens of
aircraft that would previously attempt to refuel.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2716.
2023-05-13 16:23:31 -07:00
zhexu14
5b148a74aa add tests for mission_types for various theather ground objects 2023-05-13 12:56:38 -07:00
ColonelAkirNakesh
33242048e7 Makes guard tower (house2arm) a AAA unit 2023-05-13 12:50:49 -07:00
Dan Albert
4f7932ad8a Remove old aircraft selection mode.
New mode seems to be working well.
2023-05-13 12:47:51 -07:00
Dan Albert
8158cc7112 Add livery selection dropdown to air wing config.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1861.
2023-05-13 12:33:02 -07:00
Dan Albert
50d7a3e46f Update pydcs.
More livery APIs...
2023-05-13 12:24:24 -07:00
Starfire13
b6b9a22668 Update B-52H.yaml
Added antiship mission type to B52
2023-05-12 22:21:36 -07:00
Dan Albert
bd2ec12e0f Use the actual Country type instead of the name.
We want other pieces of country information (in particular the short
names). This cleans up a lot of code anyway.

As an added bonus, this now catches squadrons that used invalid names
which would previously be passed through to pydcs and... then I don't
know what would happen.
2023-05-12 22:18:40 -07:00
Dan Albert
752a90cddb Fix broken squadron.
It's "The Netherlands", not "Netherlands".
2023-05-12 22:18:40 -07:00
Dan Albert
004594639e Update pydcs.
Includes the rewritten livery scanning code. It might need some more
tweaks to be fast enough, but it at least now doesn't spam the log for
machines that don't have DCS (or Liberation) installed, and it's not
slow until something tries to use it, so until we add the UI we won't
have to pay for it during startup.
2023-05-12 18:10:11 -07:00
ColonelAkirNakesh
6943adf6df Germany 1944 free unit list fixes.
1. Removes Flak18 as these currently require Kdo.G.40. (part of the WW2
Asset Pack) to fire at player. ED-supplied single missions are having
Flak18s w/out WW2 Asset Pack firing at a point in the air.

2. Adds Opel Blitz to frontline for more variety, balance, and some
little bit of historical accuracy. My understanding is late war was more
truck strafing than HVARing Panzers.

3. Adds guard tower to AAA units to have a functioning AAA since Flak18s
don't work.
2023-05-12 18:05:12 -07:00
Dan Albert
5ad57d2878 Move code docs to a readthedocs project.
Ported the existing docs, but the real goal is getting the docs for
campaign version moved here, as well as the manual. RTD will have an
"edit on github" button that'll keep this accessible to players, but
it'll be much easier to keep docs up to date while developing features
if it's part of the code base.
2023-05-10 23:14:04 -07:00
Dan Albert
acb2d01d92 Update pydcs.
No new data (I think), but includes a bunch of fixes for the lua parser
and rewrites some suspicious looking (but probably safe) code in the
livery scanner.

Also changes this dependency to non-editable by default. Pip chose this
automatically for me at some point, but the rules for whether or not a
py.typed file will actually be detected for an editable install are
complicated and sometimes this won't work, leading too a lot of mypy
errors. There's no need for this to be editable anyway.
2023-05-10 23:03:14 -07:00
Dan Albert
15fa73a514 Add option to limit squadron sizes and begin full.
Adding temporarily as an option to make sure it's not a terrible idea,
but the old mode will probably go away.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1583.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2808.
2023-05-05 18:16:42 -07:00
Dan Albert
7f94b34277 Add an option to prefer primary tasked aircraft.
We're still using mostly the same aircraft selection as we have before
we added squadrons: the closest aircraft is the best choice.

This adds an option to obey the primary task set by the campaign
designer (can be overridden by players), even if the squadron is farther
away than one that is capable of it as a secondary task.

I don't expect this option to live very long. I'm making it optional for
now to give people a chance to test it, but it'll either replace the old
selection strategy or will be removed.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1892.
2023-05-04 23:08:11 -07:00
Dan Albert
4d2ed64a70 Fix the command line campaign generator. 2023-05-04 23:05:21 -07:00
Dan Albert
e444761059 Support replacing squadrons in-place. 2023-05-04 20:44:27 -07:00
zhexu14
c9e4b5eba4 enable AEWC missions on FOBs 2023-05-04 20:35:51 -07:00
Dan Albert
c14c7cc73d Make patch check informational as well. 2023-05-04 18:41:27 -07:00
Dan Albert
5e459c2390 Expand python coverage, use coveragerc. 2023-05-04 10:07:34 -07:00
Dan Albert
4ee6de2c84 Make coverage checks informational.
PRs shouldn't be failing because coverage dropped. Not until our tests
aren't terrible, anyway.
2023-05-04 10:07:34 -07:00
Dan Albert
a7d2eca209 Add a test for the Aircraft component.
Leaflet (or maybe react-leaflet?) isn't very testable, so we can really
only test that mocks were called with the right props for the leaflet
components we expect, but that's still better than nothing.
2023-05-04 01:18:45 -07:00
Dan Albert
57a4a7c282 Test typescript and collect coverage.
We don't actually have any tests yet :(
2023-05-03 23:18:15 -07:00
Dan Albert
b4c5236d8b Don't fail PRs that regress coverage.
Most of our code is barely testable and our coverage isn't anything to
brag about.
2023-05-03 23:18:15 -07:00
Dan Albert
de2a779715 Gather and upload coverage of python tests. 2023-05-03 21:54:52 -07:00
zhexu14
352c2ddc56 make BAI plannable against vehicles only 2023-05-03 10:12:15 -07:00
Dan Albert
76e6aff9d7 Ignore parse errors of preferences file.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2613.
2023-05-02 00:39:18 -07:00
Dan Albert
b4c02767ac Update pydcs.
Includes the fix to prevent the livery scanner from crashing on import
when the Liberation preferences file cannot be parsed.

https://github.com/dcs-liberation/dcs_liberation/issues/2613
2023-05-02 00:39:18 -07:00
zhexu14
aa2a888ed0 Handle edge case where aircraft have built in TPGs 2023-05-02 00:34:18 -07:00
zhexu14
b50d82feff Add GBU-27 yaml 2023-05-02 00:34:18 -07:00
Dan Albert
cce9592ac8 Use task priorities from aircraft yamls.
Preferred aircraft per task are now determined by a ranking of weights
stored in the aircraft yaml files. To aid in visualizing the priorities
across aircraft, Liberation can be run with the argument
dump-task-priorities to dump a yaml file in Saved
Games/DCS/Liberation/Debug/priorities.yaml, which will show each task
along with priority sorted aircraft and their weights.

The current weights in the data were exported from the existing lists,
where each position from the bottom of the list was worth 10 (to allow
some games for less shuffling later).

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2809.
2023-04-26 23:26:23 -07:00
Dan Albert
b69def652e Add debug command to dump aircraft priorities.
https://github.com/dcs-liberation/dcs_liberation/issues/2809
2023-04-26 23:26:23 -07:00
Dan Albert
6df83485e1 Load task priorities from YAML.
Not used yet.

https://github.com/dcs-liberation/dcs_liberation/issues/2809
2023-04-26 23:26:23 -07:00
Dan Albert
e297fcbff8 Export current task priorities to aircraft yamls.
Nothing fancy. Rank in reversed (so earliest items in the original list
have the highest index) list * 10 (to leave gaps for balancing).

https://github.com/dcs-liberation/dcs_liberation/issues/2809
2023-04-26 23:26:23 -07:00
Dan Albert
2f2ebff674 Fix merged classvars in UnitType descendants.
```
>>> class Foo:
...     bar = 0
...     @classmethod
...     def set_bar(cls, v):
...             cls.bar = v
...
>>> class Bar(Foo):
...     ...
...
>>> Bar.set_bar(1)
>>> Bar.bar
1
>>> Foo.bar
0
>>> class Foo:
...     bar = {}
...     @classmethod
...     def add(cls, k, v):
...             cls.bar[k] = v
...
>>> class Bar(Foo):
...     pass
...
>>> Bar.add(0, 1)
>>> Bar.bar
{0: 1}
>>> Foo.bar
{0: 1}
```

The collections are copied by reference into the descendants, whereas
_loaded is copied by value, so that one can stay. Before this patch,
every subtype was loading because _loaded was set per subclass, but they
were all registering with a common collection defined by UnitType rather
than their own class.
2023-04-26 23:09:57 -07:00
Dan Albert
06b74c4ca6 Update mypy, fastapi, pydantic.
mypy update is needed for typing.Self support. It caught an existing bug
(missing @property on override), and fixed a bug so we can drop an
ignore.

Upgrading mypy requires upgrading pydantic to get the newest pydantic
mypy plugin, and since that's what's driving fastapi it's probably smart
to upgrade those together.
2023-04-26 23:00:23 -07:00
Dan Albert
7ddfc5e5ad Fix pydcs extension packages.
pydcs_extensions.__init__ wasn't actually doing anything because without
an __init__.py each of these "packages" was empty. This has been working
by accident because of ai_flight_planner_db.py.
2023-04-26 22:08:56 -07:00
zhexu14
f86709ebd0 Prevent decoy flights from overflying the target.
Force decoy flights to the next waypoints 120 seconds after the IP.
The duration is just an approximation, but it seems to work.

See discussion in https://github.com/dcs-liberation/dcs_liberation/pull/2810
for more details.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2781.
2023-04-26 19:30:34 -07:00
Dan Albert
7266de42f5 Format aircraft yamls. 2023-04-26 19:12:13 -07:00
Dan Albert
47831d43b5 Remember mod choices in the NGW. 2023-04-26 00:36:34 -07:00
Dan Albert
cf47dd82d7 Remember player preferences for plugins. 2023-04-26 00:24:46 -07:00
Dan Albert
081c97583b Add a plugins page to the NGW. 2023-04-26 00:07:36 -07:00
Dan Albert
77f1706cbb Extract plugins from settings.
There isn't really any need for these two types to interact. The lua
plugin manager effectively fully owned its properties, it just delegated
all reads and writes to the settings object.

Instead, break the plugin settings out into the plugin manager and
preserve the manager in the Game. This will make it possible to expose
plugin options in the NGW without breaking the game on cancel.
2023-04-25 23:28:01 -07:00
Dan Albert
664efa3ace Document Lua plugin APIs.
Trying to fix the singleton-ness in the plugin manager because it
prevents injecting settings until the game is fully committed (new game
wizard completed). Added the docs describing what I think I've been able
to discover.
2023-04-25 21:32:36 -07:00
Pekka Kiviniemi
b6059f692e Create unit tests for pilot class.
DCS Liberation does not have much unit tests. Pilot class is not very
exiting but it was a good place to start.
2023-04-26 03:03:25 +00:00
zhexu14
4bf8f25d31 Add decoy weapon type and configure AI tasks.
See https://github.com/dcs-liberation/dcs_liberation/pull/2810
for more discussion.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2780.
2023-04-25 20:02:30 -07:00
Dan Albert
e2c6d6788c Persist some campaign creation options.
We should also persist mod options, but those will go in a separate file
because they aren't a part of Settings.

Plugins need some work before that can be saved here. They're not
configurable in the NGW currently, so that needs to be fixed first. It
also appears that it may not be safe to inject the settings object with
plugin options until the game is created, but that needs more
investigation (see the comment in Settings.save_player_settings).

Another obvious candidate would be the desired player mission duration,
but we need to implement custom serialization for that first.
2023-04-24 22:49:30 -07:00
Nosajthedevil
1c20bc3966 Add DCS AI C-47 support 2023-04-24 22:49:09 -07:00
zhexu14
c31d76ec83 Reset flight's flight plan builder when changing task type.
The mechanism for how this bug arises is that the *WaypointGenerator*
uses the *FlightWaypoint.waypoint_type* to decide whether to generate
the waypoint in the .miz file using a *DeadIngressBuilder* or a
*SeadIngressBuilder*. This *waypoint_type* is set by
*ato.flightplans.<sead|dead>.Builder*, which is set when *ato.flight* is
initialised in the *Flight._flight_plan_builder* member variable based
on *Flight.flight_type*. When *Flight.flight_type* is updated when the
flight is changed from SEAD->DEAD, *Flight._flight_plan_builder* is not
updated in the development build, resulting in it continuing to generate
SEAD waypoints.

This PR adds *set_flight_type()* which sets the *flight_type* property
and updates *Flight._flight_plan_builder* and uses this function when
converting flight types. Ideally, *flight_type* should be made private
and only accessed through getter/setter functions that encapsulate this
behavior, but that would mess up any existing liberation save files.

This PR was tested by:
1. Opening the save file from Issue 2779 in the development build
2. Clicking "Take Off" and confirming that the Weapon Release Type is
"Guided" at the Ingress Waypoint as described in the issue.
3. Opening the save file from Issue 2779 in this PR
4. Converting the SEAD2DEAD flight from DEAD back to SEAD, and then from
SEAD to DEAD
5. Clicking "Take Off" and confirming in the mission editor that the
SEAD2DEAD flight has Weapon Release Type set to "Auto" at the Ingress
Waypoint.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2779.
2023-04-22 10:54:42 -07:00
zhexu14
ada8f9f8ee Fix error when resetting air wing configuration.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2751.
2023-04-22 10:51:26 -07:00
zhexu14
1b72598803 Add missing argument when fixing TOTs.
Part of https://github.com/dcs-liberation/dcs_liberation/issues/2746.
2023-04-22 10:50:20 -07:00
zhexu14
4bc8bf52e7 Also use Tanker task for recovery tankers.
DCS apparently needs both to function.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2771.
2023-04-20 17:59:14 -07:00
zhexu14
0d257a2c3b Track S_EVENT_KILL and S_EVENT_UNIT_LOST as well.
Catch a few more death signals. Still not perfect but a bit better.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2765.
2023-04-19 06:16:41 +00:00
Nosajthedevil
0ba602d3aa Added many premade squadrons.
Added a number of squadrons - US Vietnam War era squadrons for the
OV-10A and F-4B/C, US squadrons for the A-10A, iran squadrons for the
F-4E, and russian, iranian, vietnamese, syrian, and north korean
squadrons for the Mig-21.
2023-04-18 23:07:29 -07:00
dependabot[bot]
38d18ba767 Bump webpack from 5.69.1 to 5.76.1 in /client
Bumps [webpack](https://github.com/webpack/webpack) from 5.69.1 to 5.76.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.69.1...v5.76.1)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-18 11:36:12 -07:00
Dan Albert
94b8aa7213 Disallow squadrons from disabling mission types.
After this change, players will always have the final say in what
missions a squadron can be assigned to. Squadrons are not able to
influence the default auto-assignable missions either because that
property is always overridden by the campaign's air wing configuration
(the primary and secondary task properties). The `mission-types` field
of the squadron definition has been removed since it is no longer
capable of influencing anything. I haven't bothered cleaning up the now
useless data in all the existing squadrons though.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2785.
2023-04-18 11:35:41 -07:00
Dan Albert
1ac36d03da Allow in-line definitions of campaign factions.
A lot of campaigns want to define custom factions. This allows them to
do so without us having to fill the built-in factions list with a bunch
of campaign-specific factions. It also makes custom campaigns more
portable as they don't need to also distribute the custom faction files.
2023-04-17 23:52:25 -07:00
Dan Albert
dca256364a Convert existing factions to YAML. 2023-04-17 22:40:02 -07:00
Dan Albert
652e7d8d7b Allow factions to use YAML.
Comments! No more failures because you accidentally used a trailing
comma!

JSON still supported since it's basically free, but we should probably
remove that in 8 just so the docs can be less confusing for users who
will be confused if only one format is documented (and we definitely
won't maintain duplicate docs).
2023-04-17 22:40:02 -07:00
Dan Albert
42e9a6294b Remove eager loading of factions.
Eager loading meant that users would need to restart Liberation to pick
up changes to faction files. That's annoying for modders, slows down
start up, and uselessly sits in RAM when it's not needed after game
creation.

Also removes the __getitem__ and __iter__ methods in favor of named
methods, since the dunder methods are more or less impenetrable for IDEs
and grep.
2023-04-17 22:40:02 -07:00
Dan Albert
f3d2952579 Fix typo in Germany 1944.
Oops.
2023-04-17 19:31:03 -07:00
Dan Albert
ee1d4cd3e4 Replace a bunch of escape sequences.
Unify on using the real letter instead of an escape sequence for better
searchability/readability. There should be no functional change here (if
any, they should be bug fixes).

https://github.com/dcs-liberation/dcs_liberation/issues/2786
2023-04-17 18:10:25 -07:00
Dan Albert
4e067eaaa8 Remove GroundPlanners from the save state.
These are only used during mission generation. Remove them from the save
state to reduce compatibility requirements. We also have at least one
report of this data being corrupted... somehow. I don't know how that
could have happened, but if there's no data to corrupt in the first
place that's not a problem. If the corruption _does_ recur, it'll be
much easier to repro if it corrupts during mission generation rather
than during turn initialization.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2729.
2023-04-15 14:27:20 -07:00
dependabot[bot]
4a0975b21b Update Starlette and FastAPI. 2023-04-15 13:50:34 -07:00
Dan Albert
dd9ad2f0be Update pydcs for the plane helipad spawning fix.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2782.
2023-04-15 13:47:24 -07:00
Dan Albert
d1fe267072 Fix unit details screen for Qt6.
The `setMargin` shorthand was apparently to simple, so Qt6 removed it in
favor of `setContentMargins`, which does the same thing?

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2784.
2023-04-15 13:41:09 -07:00
Dan Albert
28ba100864 Fix bad character escape in Caen to Evreux.
The escape sequence looks to be valid according to the spec (although it
would need to be double quoted, but that doesn't fix it), but Python's
parser doesn't appear to handle that. Just use ü.

https://github.com/dcs-liberation/dcs_liberation/issues/2786
2023-04-15 13:27:08 -07:00
Dan Albert
99ea06c0d5 Fix file encoding for some loads.
We've actually been pretty good at getting this right in most of the
code base, but we've missed it in a few places. Python defaults to the
encoding of the current locale unless otherwise specified, and for a US
English Windows install that's cp1252, not UTF-8. I'm not brave enough
to change the locale at startup because I don't know how that might
affect CJK encoding users (or for that matter, non-Latin derived
alphabet UTF-8 variants).

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2786.
2023-04-15 13:27:08 -07:00
Dan Albert
fa070b2126 Improve logging in defaultsquadronassigner.py.
https://github.com/dcs-liberation/dcs_liberation/issues/2786
2023-04-15 13:27:08 -07:00
Dan Albert
5405632434 Improve comments in defaultsquadronassigner.py. 2023-04-15 12:33:52 -07:00
Dan Albert
ca2cec5d7d Organize imports in defaultsquadronassigner.py. 2023-04-15 12:33:52 -07:00
Starfire13
8150176fc6 Fixes F-14B squadrons that don't have SEAD as available mission type (#2777)
Fixes the YAMLs for 3 F-14B squadrons that didn't have SEAD as an
available mission type. F-14s use SEAD for TALD deployment. The other
F-14B squadrons and all the F-14A squadrons are fine. I also removed
"Bases:" from one of the squadrons as the other squadron yamls don't
have it and it's unecessary (F-14Bs can already be based at airfields
and carriers.
2023-04-09 19:18:06 -07:00
Dan Albert
5c72e0754a Fix iteration error when splitting transfers.
If one type of unit is zeroed but there are still other units to check
in the dict, the dict will have changed size during iteration. Iterate
on a copy of the dict instead.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2773.
2023-03-31 16:09:33 -07:00
Dan Albert
af2e195f90 Update pydcs to fix Falklands airport data.
The Falklands data in pydcs was missing an (unfinished, unusable)
airport, and that was breaking miz loading of campaigns that were edited
or created with a version of DCS to include these airports which have
incomplete data.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2732.
2023-03-30 17:26:05 -07:00
Nosajthedevil
8523c11357 Support for the updated F-4B/C mod (#2769)
Updated the F4.py pydcs extension to match the updated F-4B/C mod and
reworked the standard payloads to add "clean" to new F-4B pylons 11 and
12.

This includes a workaround to allow Liberation to use the new VSN F-4B
weapons with combined 2x Aim-9js on pylons 3 and 9 underslung with bombs
on ters on new pylons 11 and 12. In mission editor the combined weapons
are selected in pylons 3 and 9 and their under-slung counterparts are
forced onto 11 and 12 using "required" arguments in the mod's lua. All
other pylon 3 and 9 weapons use "required clean" arguments. Liberation
doesn't have a way to force these linkages onto pylons 11 and 12 and
without them, even without clean, no weapons will load on 3 and 9 or 11
and 12.

The workaround for normal weapons on the F-4B is to set the standard
load-outs to "clean" on pylons 11 and 12. This allows all normal weapons
to work on pylons 3 and 9 so long as pylons 11 and 12 are left as Clean.
It also allows Clean into the Liberation dropdown so it can be selected
later if necessary.

The workaround for the 4 new weapons that combines pylons 3 with 11 and
9 with 12 is that the user has to use the matching pair on each set of
pylons. For example - if F4B_LAU105_AIM9J_2_BRU42A_MK82_3 is selected
for pylon 3, BRU 42A MK823 LAU105 AIM9J2 must be selected for pylon 11.
Failure to do this correctly doesn't crash liberation or DCS, the result
will just be either no weapons at all on either pylon or the underslung
weapons on 11 and 12 floating without a pylon attaching it to the plane.

When updating f4.py in the future, note that running the pydcs database
export doesn't pull any data for Pylons 11 and 12. Those matching
weapons / classes have to be manually defined in those pylons for the
F-4B. This is noted in f4.py.
2023-03-24 18:41:03 -07:00
Dan Albert
b860d72c2d Prevent transfer splitting from leaving empty units.
If the transport were able to move exactly the quantity of units of the
given type remaining in the transfer without moving the whole order, the
transfer order would be left with instructions to transfer zero of that
unit. That's an invariant violation, and was resulting in _later_
transfers attempting to create a convoy with zero units, which pydcs
rightly rejects.

For example: if 2 Abrams and 2 Paladins are ordered to transfer from a
disconnected FOB to a distant location that is connected by road, and
only two cargo slots are available, that transfer could be split into:

```
{
    Abrams: 2
}
```

and

```
{
    Abrams: 0,
    Paladin: 2
}
```

Depending on the route, those airlifted units might end up still needing
road transit (we prefer short airlifts rather than direct routes because
it gives the player more opportunities to intercept enemy convoys). The
current turn would airlift the Abrams-only group and the Paladin group
would wait. On the next turn the Abrams would travel by road and the
Paladins would be airlifted. On the _third_ turn, the Paladins (and zero
Abrams) would generate an invalid convoy.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2761.
2023-03-18 14:03:41 -07:00
Dan Albert
bcb7d059c0 Tolerate saving when temp is on another FS.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2748.
2023-03-14 16:21:24 -07:00
Dan Albert
bc0dacf974 Fix saving of saves from other machines.
Need to reset the persisted save paths on load, since the file that was
loaded may have been moved since it was saved and the original location
may no longer be accessible.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2756.
2023-03-13 19:41:28 -07:00
Dan Albert
2d9b0177ec Add channel naming for the warthogs.
The manual for the legacy warthog usually calls these VHF 1/2 and UHF,
or VHF AM/FM and UHF. The AM/FM nomenclature is what I usually hear
people call them and it's clearer, so go with that.

For the A-10C II, that manual hasn't been updated for the AN/ARC-210
yet, so I'm not really sure what that ought to be called. The UFC calls
it COM 1 though, so I went with that. The alternative would be something
like VHF/UHF for the 210 and UHF for the 164, but I don't know if that's
actually better. Could be completely explicit and call them by their
full names, but that's probably less clear to people that aren't
fiddling with the radio implementation constantly (and even I confuse
the 164 and the 186 all the damn time).
2023-03-13 18:48:20 -07:00
Dan Albert
2fed84c676 Fix radio data for the A-10Cs.
My earlier tests were not accurate enough. It turns out that yes, DCS
does still do some channel clobbering with these, so fix the radio
selection to account for that.

I also fumbled some copy paste between the A-10Cs so the A-10C II
channel assignment ended up in the wrong section doing nothing...
2023-03-13 18:48:20 -07:00
Dan Albert
a73c06223d Update radio configuration for the A-10s.
The latest DCS update both added support for preset radios (two both
A-10C modules!), and re-ordered the legacy A-10C's radios so we can use
the VHF radio for intra-flight.

After this patch, the legacy A-10C will use VHF for intra-flight, the
new module will use one AN/ARC-210 for intra-flight and the other for
inter-flight comms, and both modules will have preset channels assigned.
2023-03-11 15:30:36 -08:00
Dan Albert
e129c02109 Update pydcs.
Includes the new A-10C radios. Will follow up to make Liberation
actually use them.

This manually removes some weapons from mod files that were removed from
DCS. Those mods may need re-exporting, and may also need updating by
their authors, but for now this should keep them functioning.
2023-03-11 15:30:36 -08:00
Dan Albert
67dae80b76 Rework the speed controls for 1080p friendliness.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2396.
2023-02-19 12:35:21 -08:00
Nosajthedevil
088b69a6ef Russian Weapon Fixes
R-24R weapons file correction to R-3R fallback name - now matches name used in R-3R file.

Remove KH-28 weapons file due to removal from DCS.

Update KH-25MP weapons file to  fallback to KH-29L instead of Kh-28. Kh-28 previously fell back to Kh-29L.
2023-02-11 16:43:12 -08:00
Nosajthedevil
db64f37a95 Update AIM-9Juli.yaml 2023-02-11 16:42:55 -08:00
Nosajthedevil
af6c42f49b Update AIM-9Juli-2X.yaml 2023-02-11 16:42:55 -08:00
Nosajthedevil
4503170075 Added US Weapons Files
Adds US Weapons files with fallbacks in order of capability.

Aim-7P added by ED for several US fighters. Falls back to 7MH, and Aim-120B fallback adjusted from MH to P.

Aim-9J and Aim-9Juli added by ED for the Mirage F-1, also used by F-4 mod and will likely be added to the F-4E whenever it ships. Aim-9J placed between  9P and 9B, Aim-9 Juli placed between 9L and 9P5.

2x9j and 2x9Juli added by VSN for F-4 mod. Placed between 2x versions of same missiles as above.

Aim-7E now falls back to 2xAim9x instead of single Aim9x so that it runs through the entire loop of Aim-9s.
2023-02-11 16:42:55 -08:00
dependabot[bot]
c07f343d0e Bump @sideway/formula from 3.0.0 to 3.0.1 in /client
Bumps [@sideway/formula](https://github.com/sideway/formula) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/sideway/formula/releases)
- [Commits](https://github.com/sideway/formula/compare/v3.0.0...v3.0.1)

---
updated-dependencies:
- dependency-name: "@sideway/formula"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-08 20:06:27 -08:00
Dan Albert
725f6c55a5 Enable sim speed controls by default.
https://github.com/dcs-liberation/dcs_liberation/issues/1680
2023-02-06 00:51:47 -08:00
Dan Albert
364742a98b Do not allow adding flights after package start.
https://github.com/dcs-liberation/dcs_liberation/issues/1680
2023-02-06 00:47:53 -08:00
Dan Albert
7b35a749e2 Prevent past startup when adding new flights.
When a new flight is added to a package, if the TOT is early enough the
new flight might have a startup time in the past. Clamp the TOT when
adding new flights to the package to avoid this.

https://github.com/dcs-liberation/dcs_liberation/issues/1680
2023-02-06 00:47:53 -08:00
Dan Albert
23ac510d26 Don't allow changing TOT for started packages.
https://github.com/dcs-liberation/dcs_liberation/issues/1680
2023-02-06 00:33:08 -08:00
Dan Albert
ba10298dbc Allow adjusting TOTs after sim start.
This makes the start time in WaitingForStart dynamic, which is more
expensive but probably still cheap enough.

It also checks that the new TOT will not result in a start time in the
past when the player changes the TOT.

https://github.com/dcs-liberation/dcs_liberation/issues/1680
2023-02-06 00:33:08 -08:00
Dan Albert
c6a8aeac1d Update bug templates for 6.1.1. 2023-02-05 15:18:09 -08:00
Raffson
b41ef0ab13 Fix heli spawn/landing at FOB/FARP
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2719.

(cherry picked from commit 34de855b8f889b72406c101d0cea385988e24bf9)
2023-02-05 14:09:01 -08:00
Dan Albert
c33a0d5deb Don't generate runway data for heliports.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2710.
2023-02-05 13:19:18 -08:00
Nosajthedevil
70b9d4c174 Add VSN F-4 Mod Support
Added VSN F-4 Mod support
2023-02-02 17:15:21 -08:00
dependabot[bot]
1462bedd97 Bump http-cache-semantics from 4.1.0 to 4.1.1 in /client
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: http-cache-semantics
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-02 17:08:17 -08:00
Dan Albert
937bacacb7 Pin version of black used in GHA.
Black rolls out style changes every year, and using "stable" means that
the check run on PRs might start formatting differently than we do
locally, or require a reformat of the codebase to make a PR submittable.

Pin to the version that we've been using. We should update to 23 at some
point, but we want to do that deliberately.
2023-02-02 16:35:51 -08:00
Dan Albert
e8824e5d03 Fix line endings. 2023-01-31 18:44:33 -08:00
Dan Albert
e030cfebb8 Update pydcs.
Export from DCS 2.8.2.35632.
2023-01-28 14:03:38 -08:00
Dan Albert
eea98b01f6 Fix invalid tanker planning.
All three refueling missions share a common task type and differentiate
their behavior based on the type and allegiance of the package target.
This means that if the theater commander identifies a carrier as the
best location for a theater refueling task, a recovery tanker will be
planned by mistake.

If this happens on a sunken carrier, mission generation will fail
because a recovery tanker cannot be generated for a sunken carrier.

Fix the crash and the misplanned theater tanker by preventing the
commander from choosing fleet control points as refueling targets.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2693.
2023-01-28 12:57:03 -08:00
Dan Albert
f8c1d291ed Update the versions listed in the bug template. 2023-01-28 11:46:37 -08:00
Dan Albert
0df268f331 Fix unit ID of the KS-19.
It seems like this unit has never worked because of the unit ID
mismatch.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2701.
2023-01-28 11:44:31 -08:00
Dan Albert
be2ad226f4 Export game.persistence.mission_path_for.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2699.
2023-01-27 16:52:45 -08:00
dependabot[bot]
a4df23361e Bump future from 0.18.2 to 0.18.3
Bumps [future](https://github.com/PythonCharmers/python-future) from 0.18.2 to 0.18.3.
- [Release notes](https://github.com/PythonCharmers/python-future/releases)
- [Changelog](https://github.com/PythonCharmers/python-future/blob/master/docs/changelog.rst)
- [Commits](https://github.com/PythonCharmers/python-future/compare/v0.18.2...v0.18.3)

---
updated-dependencies:
- dependency-name: future
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-16 17:26:34 -08:00
Dan Albert
0f34946127 Restructure save games into a zipped bundle.
This is the first step toward bundling all assets related to a save game
into a single item. That makes it easier to avoid clobbering "temporary"
assets from other games like the state.json, but also makes it easier
for players to file bug reports, since there's only a single asset to
upload.

This is only the first step because so far it only includes the various
save files: start of turn, end of last turn before results processing,
and "latest" (the game saved explicitly by the player).
2023-01-16 13:59:16 -08:00
MetalStormGhost
575470ae1b Updated Community A-4E-C mod version support to 2.1.0 release. 2023-01-09 23:28:22 -08:00
dependabot[bot]
31c59e7380 Bump json5 from 1.0.1 to 1.0.2 in /client
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-07 21:25:21 -08:00
Dan Albert
4b542b70ae Fix post-results "last turn" save. 2023-01-04 15:15:56 -08:00
Dan Albert
9cb641bddf Allow flights to self-initialize.
This makes it possible to add new packages to a running sim.

https://github.com/dcs-liberation/dcs_liberation/issues/1680
2023-01-04 12:58:36 -08:00
Dan Albert
7a8b3591cd Add locking to some UI actions.
This is by no means complete. The bugs that this solves were already in
6.x, but we'd hidden the speed controls for the sim in that release, and
have always said that anything done after pressing "go" the first time
is undefined behavior. This is the first step on making those mid-sim
actions behave correctly.

UI actions such as creating a new package need to be executed between
ticks of the sim. We can either do this synchronously by blocking the UI
until the tick is done executing, acquiring a lock on the sim, executing
the action, then releasing the lock; or asynchronously by queueing
events and letting the sim execute them when it completes the current
tick (or instantly if the sim is paused).

Anything that comes from the new UI (currently just the map) must be
asynchronous because it goes through the REST API, but for the old UI
it's simpler (and because the lock will only be acquired as quickly as
the user can act, shouldn't slow anything down) to do this
synchronously, since it's difficult to use coroutines in Qt.

https://github.com/dcs-liberation/dcs_liberation/issues/1680
2023-01-04 12:58:36 -08:00
Dan Albert
fd2ba6b2b2 Convert TOTs to datetime.
https://github.com/dcs-liberation/dcs_liberation/issues/1680
2023-01-04 12:58:36 -08:00
Dan Albert
ac6cc39616 Fix AdjustToContents use for Qt6.
This is no longer a property exposed directly on QComboBox.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2684.
2023-01-04 12:30:43 -08:00
Dan Albert
0be9e1985a Upgrade to Python 3.11.
Supposedly much faster. Probably other cool stuff.
2022-12-29 17:47:49 -08:00
Dan Albert
a1af4e563a Python 3.11 compatibility.
https://docs.python.org/3/library/asyncio-task.html#asyncio.wait says:

> Changed in version 3.11: Passing coroutine objects to wait() directly
> is forbidden.
2022-12-29 17:47:49 -08:00
Dan Albert
7167e84a8f Add back asgiref.
uvicorn now needs this for type information. Not sure why this works
with Python 3.10.
2022-12-29 17:47:49 -08:00
Dan Albert
7eeb84de47 Upgrade lots of dependencies.
Cleared the requirements.txt and rebuilt from scratch. The only thing
left un-upgraded is mypy, which I'll deal with separately because it
catches new issues.
2022-12-29 17:09:42 -08:00
Dan Albert
45aabf369b Upgrade Shapely. 2022-12-29 16:48:42 -08:00
Dan Albert
e396a21791 Update pyproj. 2022-12-29 16:47:29 -08:00
Dan Albert
c6635a4885 Update FastAPI and friends. 2022-12-29 16:43:43 -08:00
Dan Albert
306971230b Update to PySide6.
It sounds like PySide2 will not be moving to Python 3.11, so we're stuck
on 3.10 without this. Upgrading to a newer Qt also fixes some high DPI
bugs (the file browser dialog for save/load is no longer tiny on 4k).

https://github.com/pyinstaller/pyinstaller/issues/5414 previously
blocked this, but the bug appears to be fixed now.
2022-12-29 16:26:50 -08:00
Dan Albert
e0c13846a7 Upgrade pyinstaller.
Should include https://github.com/pyinstaller/pyinstaller/issues/5414.
2022-12-29 15:39:50 -08:00
Dan Albert
4aa42e6573 Remove isinstance use in flight support data.
mypy struggles to prove this cast correct when there are two or'd
isinstance checks where both types coincidentally have properties of the
same name (but no defined protocol making that explicit). I'm not really
sure why mypy is happy with this in its current state, but it isn't
after a change I'm making.

All our isinstance use is a bit of an anti-pattern anyway, so extract a
method that exposes the data we care about.

The start/end times for tankers aren't actually used, so this could be
simplified even more, but that data _should_ be used.
2022-12-28 14:02:31 -08:00
Dan Albert
24a04fb8c6 Workaround pycharm debugger issue when used.
Works around
https://youtrack.jetbrains.com/issue/PY-53232/Debugger-doesnt-work-when-pyproj-is-imported
whenever the pycharm debugger is detected.
2022-12-28 14:00:45 -08:00
SnappyComebacks
321de8d4ec Set units on the frontline to Hidden On MFD. (#2669)
All groups (friendly and enemy) that are part of the front line are set
to `Hidden On MFD`. This is a group level filter, and can not be applied
on a per unit basis.
2022-12-28 14:19:33 -07:00
Dan Albert
5db82f733f Revert "Fix for orbit's broken stop condition"
ED fixed the bug.

This reverts commit 73ee2ba4c0.
2022-12-28 12:28:39 -08:00
Dan Albert
d65fbf299c Note that we don't support stable DCS. 2022-12-21 18:06:52 -08:00
Dan Albert
7c2bb3bd85 Remove note about DCS bug.
This was fixed in
https://www.digitalcombatsimulator.com/en/news/changelog/openbeta/2.8.1.34437/

> Fixed: Stop Condition "Time More" is broken.
2022-12-21 13:29:23 -08:00
RndName
f9903f1e19 Only check for ground units in capture trigger zone
requires pydcs update: https://github.com/pydcs/dcs/pull/279
2022-12-21 13:04:09 -08:00
RndName
4a4935f165 Change iads command unit type 2022-12-21 13:04:09 -08:00
RndName
09f92cc5e4 Only add skynet iads command unit when advanced iads is in use 2022-12-21 13:04:09 -08:00
Dan Albert
887e5997c2 Update pydcs.
Includes UnitType parameter of AllOfCoalition in/out zone condition.
2022-12-21 12:56:17 -08:00
Dan Albert
f88a50dd07 Update changelog for Blackshark 3.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2657.
2022-12-21 12:29:45 -08:00
Dan Albert
3f12a5ae3d Add icons and banners for BS3.
https://github.com/dcs-liberation/dcs_liberation/issues/2657
2022-12-21 12:29:45 -08:00
Dan Albert
f2946817bf Add BS3 loadouts.
https://github.com/dcs-liberation/dcs_liberation/issues/2657
2022-12-21 12:29:45 -08:00
Dan Albert
935a9b0631 Add BS3 to factions that have BS2.
https://github.com/dcs-liberation/dcs_liberation/issues/2657
2022-12-21 12:29:45 -08:00
Dan Albert
c0dc411102 Add blackshark 3 yaml.
There are no significant notable changes from Blackshark 2, so this is
the same YAML.

https://github.com/dcs-liberation/dcs_liberation/issues/2657
2022-12-21 12:29:45 -08:00
Dan Albert
55037626a4 Add Blackshark 3 to the mission planning DB.
https://github.com/dcs-liberation/dcs_liberation/issues/2657
2022-12-21 12:29:45 -08:00
Dan Albert
fc3e72bacf Fix type-only import that needs to be real.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2660.
2022-12-21 12:26:41 -08:00
SnappyComebacks
0fd0f0e7c0 Recovery tanker follow up (#2659)
Fix timing issues for RecoveryTankerFlightPlan.

AEWC and Refueling can be planned on LHA.
2022-12-20 21:47:50 -07:00
Dan Albert
22503d4e95 Save the last turn for bug reports.
We often get save games uploaded with bug reports that are already in a
broken state with nothing we can do about it. This saves that turn to
`last_turn.liberation` so users are less likely to have clobbered the
useful data before filing the report.
2022-12-20 13:46:21 -08:00
Dan Albert
de9236e93a Fix the channel's landmap.
Caused by a bad rename when I did the migration. The landmap in 6.0.0
was actually a GIF...

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2612.
2022-12-20 12:59:38 -08:00
Dan Albert
66523301aa Update changelog for pydcs update. 2022-12-20 12:31:22 -08:00
SnappyComebacks
9a81121ac1 Recovery Tanker for carriers. (#2649)
Implement recovery tankers for carriers.

UnitMap gets a little more data to store.  Recovery tankers depend on the unit map.
2022-12-19 21:08:19 -07:00
SnappyComebacks
a245ba80c3 Update F-15E loadouts. 2022-12-14 21:45:49 -07:00
Dan Albert
9a1860fc5e Add new British navy units to the UK faction.
I haven't removed the old US navy stuff from this faction, since all the
new UK ships are frigates, and removing the US stuff would deprive the
faction of cruisers and destroyers, which might break generation (I'm
not familiar enough with the new system to say for sure).

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2571.
2022-12-13 00:05:44 -08:00
Dan Albert
f3f5ab70ea Add HMS Invincible as a valid runway. 2022-12-13 00:05:44 -08:00
Dan Albert
6ce7638fdc Fix changelog formatting. 2022-12-12 23:35:37 -08:00
ColonelAkirNakesh
7f916d55e7 Add HMS Invincible. 2022-12-12 23:19:29 -08:00
Dan Albert
43ea019091 Add missing changelog note. 2022-12-12 19:32:29 -08:00
Dan Albert
6025cad716 Fix livery for VF-33 F-14A squadron.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2610.
2022-12-12 19:11:46 -08:00
Dan Albert
9365aea724 Fix encoding issues with the Peru faction.
Not really sure what's going on here, but presumably it's UTF-16 and
UTF-8 fighting.
2022-12-12 18:53:18 -08:00
Dan Albert
28859a8a9c Add changelog notes that were missed in PRs. 2022-12-12 18:52:28 -08:00
DillieKoe
304fd7ea80 Add Peru faction.
A new Peru faction with a mirage squadon
2022-12-12 18:39:21 -08:00
ColonelAkirNakesh
5e345263a7 Override liveries for Russian aircraft in bluefor faction. 2022-12-12 18:21:12 -08:00
dependabot[bot]
445ee25bbf Bump certifi from 2022.6.15 to 2022.12.7
Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.6.15 to 2022.12.7.
- [Release notes](https://github.com/certifi/python-certifi/releases)
- [Commits](https://github.com/certifi/python-certifi/compare/2022.06.15...2022.12.07)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-08 23:21:18 -08:00
Dan Albert
20937815f8 Fix CAS not having landing waypoints.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2611.
2022-12-03 16:30:51 -08:00
RndName
3863b8ef40 Fix manual debrief submit not ending mission 2022-11-30 13:20:31 +01:00
Dan Albert
d91ccaa70f Changelog section for 6.1. 2022-11-27 21:15:22 -08:00
ColonelAkirNakesh
7673ca5481 Add support for Leander class HMS Andromeda.
https://github.com/dcs-liberation/dcs_liberation/issues/2571
2022-11-27 14:22:41 -08:00
ColonelAkirNakesh
774a37a7d2 Add support for Leander class HMS Ariadne.
https://github.com/dcs-liberation/dcs_liberation/issues/2571
2022-11-27 14:11:19 -08:00
ColonelAkirNakesh
905094f63f Add support for Castle Class.
https://github.com/dcs-liberation/dcs_liberation/issues/2571
2022-11-27 14:10:52 -08:00
ColonelAkirNakesh
fd5b7ba49d Add support for Leander class HMS Achilles.
https://github.com/dcs-liberation/dcs_liberation/issues/2571
2022-11-27 14:00:36 -08:00
SnappyComebacks
1b828b95b3 Add F-15E to DEAD_CAPABLE in AI flight planner. 2022-11-26 09:47:08 -07:00
Dan Albert
54546aaefb Expand gitattributes to cover common files.
Specifically adding this so that yaml changes stop being uploaded as
CRLF, but the rest is likely useful too.
2022-11-25 15:36:04 -08:00
Dan Albert
ded5fc8b1d Update bug templates to 6.0.0.
We're not fixing 5.x bugs any more.
2022-11-25 15:26:20 -08:00
Dan Albert
68fc4f6950 Remove incompatible campaigns.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2558.
2022-11-25 15:17:30 -08:00
Starfire13
5d07238ab8 Updates Khopa's Normandy and Channel campaign to 10.0
Note that Operation Dynamo only requires a yaml file update. The .miz file is fine and is not included here.
2022-11-25 15:08:25 -08:00
Dan Albert
5e7e5e2636 Unfilter the custom waypoint targets.
There doesn't appear to be any reason for us to be poking at
implementation details here aside from changing the name from "unit" to
"building" for that case. Just iterate over the known strike targets.

Making this change uncovered some latent type errors.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2564.
2022-11-25 14:14:38 -08:00
Dan Albert
ca5c0055d1 Remove dead code. 2022-11-25 14:14:38 -08:00
Dan Albert
e208df16b2 Add radios for the MB-339A.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2511.
2022-11-25 14:07:56 -08:00
Dan Albert
11632b0ef1 Update RoleplayingPleb's campaigns.
https://github.com/dcs-liberation/dcs_liberation/issues/2558
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2561.
2022-11-25 13:05:20 -08:00
Dan Albert
b0bc46f539 Make the casualty report scrollable.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2567.
2022-11-25 13:00:04 -08:00
SnappyComebacks
627ed45065 Add banner for MB-339A. 2022-11-25 11:58:56 -07:00
RndName
fc9ad5b519 Remove dcs capture event from state json
With the latest change we added capture zones and corresponding trigger rules for all Airfields as well so we do not need to rely on the dcs capture event S_EVENT_BASE_CAPTURED anymore.
2022-11-24 11:14:50 +01:00
RndName
40ddad1d9a Add Airfield to list of capture zone types
This will create capture zones and the trigger rules to check for a base capture. Will fix an issue where the dcs capture event is not fired and therefore the capture not recognized by liberation
2022-11-24 11:14:50 +01:00
RndName
eb997db703 Fix carrier group generation 2022-11-23 12:57:43 +01:00
MetalStormGhost
e53dc5b80b Attempt at fixing Carrier killed in state.json but not being removed from game, issue #2405. GenericCarrierGenerator.generate() will now generate the ship group with an array that only contains alive ship units, just like GroundObjectGenerator.generate() has previously done.
Carrier groups will now also show up as destroyed/damaged on the map when the carrier is sunk.
2022-11-23 12:57:43 +01:00
RndName
ab64655f05 Validate primary and secondary nodes for iads network 2022-11-21 12:13:49 +01:00
RndName
e1b530e4fc Fix IADS network error caused by dead groups
Fixed an error which would occur when dead units which are non static would be added as secondary node during the skynet lua data generation. This should in general not be possible as connection nodes and power sources are currently most of the time static.
2022-11-21 12:13:49 +01:00
SnappyComebacks
4414853e45 Update pydcs.
Added ice halo generation.
2022-11-20 17:27:35 -07:00
Dan Albert
35adcd2c7f Move develop forward to 7.0. 2022-11-20 12:57:42 -08:00
727 changed files with 14953 additions and 10898 deletions

8
.coveragerc Normal file
View File

@@ -0,0 +1,8 @@
[report]
exclude_lines =
pragma: no cover
if TYPE_CHECKING:
[run]
branch = True
source = game,pydcs_extensions,qt_ui,resources/tools

76
.gitattributes vendored
View File

@@ -14,3 +14,79 @@
*.pyo binary export-ignore
*.pyd binary
unshipped_data/arcgis_maps/ filter=lfs diff=lfs merge=lfs -text
# https://github.com/alexkaratarakis/gitattributes/blob/master/Common.gitattributes
# Documents
*.bibtex text diff=bibtex
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
*.md text diff=markdown
*.mdx text diff=markdown
*.tex text diff=tex
*.adoc text
*.textile text
*.mustache text
*.csv text
*.tab text
*.tsv text
*.txt text
*.sql text
*.epub diff=astextplain
# Graphics
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.tif binary
*.tiff binary
*.ico binary
# SVG treated as text by default.
*.svg text
# If you want to treat it as binary,
# use the following line instead.
# *.svg binary
*.eps binary
# Scripts
*.bash text eol=lf
*.fish text eol=lf
*.sh text eol=lf
*.zsh text eol=lf
# These are explicitly windows files and should use crlf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
# Serialisation
*.json text
*.toml text
*.xml text
*.yaml text
*.yml text
# Archives
*.7z binary
*.gz binary
*.tar binary
*.tgz binary
*.zip binary
# Text files where line endings should be preserved
*.patch -text
#
# Exclude files from exporting
#
.gitattributes export-ignore
.gitignore export-ignore
.gitkeep export-ignore

View File

@@ -31,12 +31,13 @@ body:
If the bug was found in a development build, select "Development build"
and provide a link to the build in the field below.
options:
- 5.2.0
- 6.1.1
- Development build
- type: textarea
attributes:
label: Build information
description: The build information from the Help -> Report an issue window.
description:
The build information from the Help -> Report an issue window.
- type: textarea
attributes:
label: Description
@@ -52,7 +53,7 @@ body:
description: >
Attach any files needed to reproduce the bug here. **A save game is
required.** We typically cannot help without a save game (the
`.liberation` file found in
`.liberation` (or `.liberation.zip`, for 7.x) file found in
`%USERPROFILE%/Saved Games/DCS/Liberation/Saves`), so most bugs filed
without saved games will be closed without investigation.
@@ -60,10 +61,9 @@ body:
Other useful files to include are:
The Liberation log file. The log file is located at
`<Liberation install directory>/logs/liberation.log`. The log often
includes data about non-fatal errors that could be the root cause of the
problem.
The Liberation log file. The log file is located at `<Liberation install
directory>/logs/liberation.log`. The log often includes data about
non-fatal errors that could be the root cause of the problem.
The `liberation_nextturn.miz` or a track file. This should always be
@@ -73,11 +73,14 @@ body:
The `state.json` file for the most recently completed turn, located at
`<Liberation install directory>/state.json`. This file is essential for
investigating any issues with end-of-turn results processing.
investigating any issues with end-of-turn results processing. **If you
include this file, also include `last_turn.liberation`** (unless the
save is from 7.x or newer, which includes that information in the save
automatically).
You can attach files to the bug by dragging and dropping the file
into this text box. GitHub will not allow uploads of all file types, so
You can attach files to the bug by dragging and dropping the file into
this text box. GitHub will not allow uploads of all file types, so
attach a zip of the files if needed.
validations:
required: true

View File

@@ -39,12 +39,13 @@ body:
If the bug was found in a development build, select "Development build"
and provide a link to the build in the field below.
options:
- 5.2.0
- 6.1.1
- Development build
- type: textarea
attributes:
label: Build information
description: The build information from the Help -> Report an issue window.
description:
The build information from the Help -> Report an issue window.
- type: input
attributes:
label: Campaign name
@@ -59,8 +60,8 @@ body:
label: Blue faction
description: >
The name of the blue faction you selected. If the bug only occurs with a
custom faction (or modifications to a stock faction), upload the
faction file as an attachment to the bug description field.
custom faction (or modifications to a stock faction), upload the faction
file as an attachment to the bug description field.
validations:
required: true
- type: input
@@ -68,8 +69,8 @@ body:
label: Red faction
description: >
The name of the red faction you selected. If the bug only occurs with a
custom faction (or modifications to a stock faction), upload the
faction file as an attachment to the bug description field.
custom faction (or modifications to a stock faction), upload the faction
file as an attachment to the bug description field.
validations:
required: true
- type: textarea
@@ -102,11 +103,11 @@ body:
attributes:
label: Log file
description: >
Attach the Liberation log file. The log file is located at
`<Liberation install directory>/logs/liberation.log`.
Attach the Liberation log file. The log file is located at `<Liberation
install directory>/logs/liberation.log`.
You can attach files to the bug by dragging and dropping the file
into this text box.
You can attach files to the bug by dragging and dropping the file into
this text box.
validations:
required: true

View File

@@ -6,7 +6,7 @@ runs:
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.10"
python-version: "3.11"
cache: pip
- name: Install environment
@@ -19,5 +19,3 @@ runs:
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

View File

@@ -11,6 +11,7 @@ jobs:
- uses: actions/setup-python@v2
- uses: psf/black@stable
with:
version: ~=22.12
src: "."
options: "--check"

View File

@@ -15,4 +15,24 @@ jobs:
- name: run tests
run: |
./venv/scripts/activate
pytest tests
pytest --cov-report=xml tests
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
ts-tests:
name: Typescript tests
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Set up JS environment
uses: ./.github/actions/setup-liberation-js
- name: run tests
run: |
cd client
npm test -- --coverage
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3

3
.gitignore vendored
View File

@@ -1,11 +1,14 @@
*.pyc
__pycache__
build/**
# Sphinx
docs/_build
resources/payloads/*.lua
venv
.DS_Store
.vscode/settings.json
dist/**
/.coverage
# User-specific stuff
.idea/
.env

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/psf/black
rev: 22.6.0
rev: 22.12.0
hooks:
- id: black
language_version: python3

13
.readthedocs.yaml Normal file
View File

@@ -0,0 +1,13 @@
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.11"
sphinx:
configuration: docs/conf.py
python:
install:
- requirements: docs/requirements.txt

View File

@@ -14,7 +14,12 @@
## About DCS Liberation
DCS Liberation is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player or co-op dynamic campaign.
It is an external program that generates full and complex DCS missions and manage a persistent combat environment.
It is an external program that generates full and complex DCS missions and manage a persistent combat environment.
**Note that DCS Liberation does not support the stable release of DCS. We can
only guarantee compatibility with either the open beta or the stable release,
and more people play the open beta. DCS stable _might_ work sometimes, but it's
untested, and we will be unable to fix any bugs unique to stable DCS.**
![Screenshot](https://user-images.githubusercontent.com/315852/120939254-0b4a9f80-c6cc-11eb-82f5-ce3f8d714bfe.png)
@@ -29,7 +34,6 @@ To download preview builds of the next version of DCS Liberation, see https://gi
These DCS bugs prevent us from improving AI behavior. Please upvote them! (But please
_don't_ spam them with comments):
* [Hold points do not work in DCS 2.8](https://forum.dcs.world/topic/311458-humvee-ground-unit-holdstop-conditiontime-more-bug-28-mission-editor/)
* [A2A and SEAD escorts don't escort](https://forums.eagle.ru/topic/251798-options-for-alternate-ai-escort-behavior/?tab=comments#comment-4668033)
* [DEAD can't use mixed loadouts effectively](https://forums.eagle.ru/topic/271941-ai-rtbs-after-firing-decoys-despite-full-load-of-bombs/)

View File

@@ -1,3 +1,87 @@
# 7.0.0
Saves from 6.x are not compatible with 7.0.
## Features/Improvements
* **[Engine]** Support for DCS 2.8.5.40170.
* **[Engine]** Saved games are now a zip file of save assets for easier bug reporting. The new extension is .liberation.zip. Drag and drop that file into bug reports.
* **[Campaign]** Added options to limit squadron sizes and to begin all squadrons at maximum strength. Maximum squadron size is defined during air wing configuration with default values provided by the campaign.
* **[Campaign]** Added handling for more DCS death events. This probably does not catch any deaths that weren't previously tracked, but it should record them sooner, which will improve results for game crashes or other early exits.
* **[Campaign AI]** The campaign AI now prefers fulfilling missions with squadrons which have a matching primary task. Previously distance from target held a stronger influence than task preference. Primary tasks for squadrons are set by campaign designers but are user-configurable.
* **[Flight Planning]** Package TOT and composition can be modified after advancing time in Liberation.
* **[Mission Generation]** Units on the front line are now hidden on MFDs.
* **[Mission Generation]** Preset radio channels will now be configured for both A-10C modules.
* **[Mission Generation]** The A-10C II now uses separate radios for inter- and intra-flight comms (similar to other modern aircraft).
* **[Mission Generation]** Wind speeds no longer follow a uniform distribution. Median wind speeds are now much lower and the standard deviation has been reduced considerably at altitude but increased somewhat at MSL.
* **[Mission Generation]** Improved task generation for SEAD flights carrying TALDs.
* **[Mission Generation]** Added task timeout for SEAD flights with TALDs to prevent AI from overflying the target.
* **[Mission Generation]** Game state will automatically be checkpointed before fast-forwarding the mission, and restored on mission abort. This means that it's now possible to abort a mission and make changes without needing to manually re-load your game.
* **[Modding]** Updated Community A-4E-C mod version support to 2.1.0 release.
* **[Modding]** Add support for VSN F-4B and F-4C mod.
* **[Modding]** Added support for AI C-47 mod.
* **[Modding]** Custom factions can now be defined in YAML as well as JSON. JSON support may be removed in the future if having both formats causes confusion.
* **[Modding]** Campaigns which require custom factions can now define those factions directly in the campaign YAML. See Operation Aliied Sword for an example.
* **[Modding]** The `mission_types` field in squadron files has been removed. Squadron task capability is now determined by airframe, and the auto-assignable list has always been overridden by the campaign settings.
* **[Modding]** Aircraft task capabilities and preferred aircraft for each task are now moddable in the aircraft unit yaml files. Each aircraft has a weight per task. Higher weights are given higher preference.
* **[Modding]** Wind speed generation inputs are now moddable. See https://dcs-liberation.rtfd.io/en/latest/modding/weather.html.
* **[New Game Wizard]** Choices for some options will be remembered for the next new game. Not all settings will be preserved, as many are campaign dependent.
* **[New Game Wizard]** Lua plugins can now be set while creating a new game.
* **[New Game Wizard]** Squadrons can be directly replaced with a preset during air wing configuration rather than needing to remove and create a new squadron.
* **[New Game Wizard]** Squadron liveries can now be selected during air wing configuration.
* **[Squadrons]** Squadron-specific mission capability lists no longer restrict players from assigning missions outside the squadron's preferences.
* **[UI]** The orientation of objects like SAMs, EWRs, garrisons, and ships can now be manually adjusted.
## Fixes
* **[Campaign]** Fixed a longstanding bug where oversized airlifts could corrupt a save with empty convoys.
* **[Campaign]** Aircraft with built-in TGPs but without an external pod will no longer degrade automatic loadouts to iron bombs.
* **[Engine]** Fixed crash in startup caused by a corrupted Liberation preferences file.
* **[Flight Planning]** AEW&C missions are now plannable over FOBs and LHAs.
* **[Flight Planning]** BAI is no longer plannable against buildings.
* **[Modding]** Fixed an issue where Falklands campaigns created or edited with new versions of DCS could not be loaded.
* **[Modding]** Fixed decoding of campaign yaml files to use UTF-8 rather than the system locale's default. It's now possible to use "Bf 109 K-4 Kurfürst" as a preferred aircraft type.
* **[Mission Generation]** Planes will no longer spawn in helipads that are not also designated for fixed wing parking.
* **[Mission Generation]** Potentially an issue where ground war planning game state could become corrupted, preventing mission generation.
* **[Mission Generation]** Refueling tasks will now only be created for flights that have a tanker in their package.
* **[Mission Generation]** Fixed missing Tanker task on recovery tanker missions.
* **[UI]** Fixed error when resetting air wing configuration during game setup.
* **[UI]** Fixed flight plan recreation when changing mission type with "Recreate as" flight options.
* **[UI]** Fixed failure to launch UI when Liberation persistent preferences file was corrupt.
# 6.1.1
## Fixes
* **[Data]** Fixed unit ID for the KS-19 AAA. KS-19 would not previously generate correctly in missions. A new game is required for this fix to take effect.
* **[Flight Planning]** Automatic flight planning will no longer accidentally plan a recovery tanker instead of a theater refueling package. This fixes a potential crash during mission generation when opfor plans a refueling task at a sunk carrier. You'll need to skip the current turn to force opfor to replan their flights to get the fix.
* **[Mission Generation]** Using heliports (airports without any runways) will no longer cause mission generation to fail.
* **[Mission Generation]** Prevent helicopters from spawning into collisions at FARPs when more than one flight uses the same FARP.
# 6.1.0
Saves from 6.0.0 are compatible with 6.1.0
## Features/Improvements
* **[Engine]** Support for DCS 2.8.1.34437, including Blackshark 3.
* **[Factions]** Defaulted bluefor modern to use Georgian and Ukrainian liveries for Russian aircraft.
* **[Factions]** Added Peru.
* **[Flight Planning]** AEW&C and Refueling flights are now plannable on LHA carriers.
* **[Flight Planning]** Refueling flights planned on aircraft carriers will act as a recovery tanker for the carrier.
* **[Loadouts]** Adjusted F-15E loadouts.
* **[Mission Generation]** The previous turn will now be saved as last_turn.liberation when submitting mission results. This is often essential for debugging bug reports. **Include this file in the bug report whenever it is available.**
* **[Modding]** Added support for the HMS Ariadne, Achilles, and Castle class.
* **[Modding]** Added HMS Invincible to the game data as a helicopter carrier.
## Fixes
* **[Flight Planning]** Fixes CAS flights not having landing waypoints.
* **[Mission Generation]** Airbase and FOB capture is no longer blocked by grounded aircraft / helicopters.
* **[Squadrons]** Fixed the livery for the VF-33 F-14A squadron.
* **[Theaters]** Fixed Channel campaigns not having data for land/sea/obstacle boundaries, causing front lines to extend into forests and water. Requires a new campaign to get the fix.
* **[UI]** Fixed an issue where manual submit of mission results did not end the mission correctly.
# 6.0.0
Saves from 5.x are not compatible with 6.0.
@@ -16,6 +100,7 @@ Saves from 5.x are not compatible with 6.0.
* **[Mission Generation]** Added performance option to not cull IADS when culling would affect how mission is played at target area.
* **[Mission Generation]** Reworked the ground object generation which now uses a new layout system
* **[Mission Generation]** Added information about the modulation (AM/FM) of the assigned frequencies to the kneeboard and assign AM modulation instead of FM for JTAC.
* **[Mission Generation]** Added ice halos.
* **[Mission Generation]** Adjusted wind speeds. Wind speeds at high altitude are generally higher now.
* **[Mission Generation]** Added turbulence. Higher in Summer and Winter, also higher at day time than at nighttime.
* **[Modding]** Updated UH-60L mod version support to 1.3.1

149
client/package-lock.json generated
View File

@@ -41,6 +41,7 @@
"electron": "^21.1.0",
"electron-is-dev": "^2.0.0",
"generate-license-file": "^2.0.0",
"identity-obj-proxy": "^3.0.0",
"license-checker": "^25.0.1",
"react-scripts": "5.0.1",
"ts-node": "^10.9.1",
@@ -3914,9 +3915,9 @@
}
},
"node_modules/@sideway/formula": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
"integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
"dev": true
},
"node_modules/@sideway/pinpoint": {
@@ -5623,9 +5624,9 @@
}
},
"node_modules/acorn": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
@@ -6204,9 +6205,9 @@
}
},
"node_modules/babel-loader/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"dependencies": {
"minimist": "^1.2.0"
@@ -8412,9 +8413,9 @@
}
},
"node_modules/enhanced-resolve": {
"version": "5.9.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.1.tgz",
"integrity": "sha512-jdyZMwCQ5Oj4c5+BTnkxPgDZO/BJzh/ADDmKebayyzNwjVX1AFCeGkOfxNx0mHi2+8BKC5VxUYiw3TIvoT7vhw==",
"version": "5.12.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
"integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.4",
@@ -10670,9 +10671,9 @@
}
},
"node_modules/http-cache-semantics": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"dev": true
},
"node_modules/http-deceiver": {
@@ -10816,7 +10817,7 @@
"node_modules/identity-obj-proxy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz",
"integrity": "sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ=",
"integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==",
"dev": true,
"dependencies": {
"harmony-reflect": "^1.4.6"
@@ -13481,12 +13482,6 @@
"integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==",
"dev": true
},
"node_modules/json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
"dev": true
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
@@ -13519,13 +13514,10 @@
"optional": true
},
"node_modules/json5": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"dependencies": {
"minimist": "^1.2.5"
},
"bin": {
"json5": "lib/cli.js"
},
@@ -19401,9 +19393,9 @@
}
},
"node_modules/tsconfig-paths/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"dependencies": {
"minimist": "^1.2.0"
@@ -19856,9 +19848,9 @@
}
},
"node_modules/watchpack": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
"integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
"integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
"dev": true,
"dependencies": {
"glob-to-regexp": "^0.4.1",
@@ -19896,9 +19888,9 @@
}
},
"node_modules/webpack": {
"version": "5.69.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.69.1.tgz",
"integrity": "sha512-+VyvOSJXZMT2V5vLzOnDuMz5GxEqLk7hKWQ56YxPW/PQRUuKimPqmEIJOx8jHYeyo65pKbapbW464mvsKbaj4A==",
"version": "5.76.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz",
"integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==",
"dev": true,
"dependencies": {
"@types/eslint-scope": "^3.7.3",
@@ -19906,24 +19898,24 @@
"@webassemblyjs/ast": "1.11.1",
"@webassemblyjs/wasm-edit": "1.11.1",
"@webassemblyjs/wasm-parser": "1.11.1",
"acorn": "^8.4.1",
"acorn": "^8.7.1",
"acorn-import-assertions": "^1.7.6",
"browserslist": "^4.14.5",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.8.3",
"enhanced-resolve": "^5.10.0",
"es-module-lexer": "^0.9.0",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.2.9",
"json-parse-better-errors": "^1.0.2",
"json-parse-even-better-errors": "^2.3.1",
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
"schema-utils": "^3.1.0",
"tapable": "^2.1.1",
"terser-webpack-plugin": "^5.1.3",
"watchpack": "^2.3.1",
"watchpack": "^2.4.0",
"webpack-sources": "^3.2.3"
},
"bin": {
@@ -23678,9 +23670,9 @@
}
},
"@sideway/formula": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
"integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
"dev": true
},
"@sideway/pinpoint": {
@@ -25020,9 +25012,9 @@
}
},
"acorn": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
"dev": true
},
"acorn-globals": {
@@ -25449,9 +25441,9 @@
},
"dependencies": {
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"requires": {
"minimist": "^1.2.0"
@@ -27135,9 +27127,9 @@
}
},
"enhanced-resolve": {
"version": "5.9.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.1.tgz",
"integrity": "sha512-jdyZMwCQ5Oj4c5+BTnkxPgDZO/BJzh/ADDmKebayyzNwjVX1AFCeGkOfxNx0mHi2+8BKC5VxUYiw3TIvoT7vhw==",
"version": "5.12.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
"integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.2.4",
@@ -28835,9 +28827,9 @@
}
},
"http-cache-semantics": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"dev": true
},
"http-deceiver": {
@@ -28947,7 +28939,7 @@
"identity-obj-proxy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz",
"integrity": "sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ=",
"integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==",
"dev": true,
"requires": {
"harmony-reflect": "^1.4.6"
@@ -30892,12 +30884,6 @@
"integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==",
"dev": true
},
"json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
"dev": true
},
"json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
@@ -30930,13 +30916,10 @@
"optional": true
},
"json5": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true
},
"jsonfile": {
"version": "6.1.0",
@@ -35297,9 +35280,9 @@
},
"dependencies": {
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"requires": {
"minimist": "^1.2.0"
@@ -35647,9 +35630,9 @@
}
},
"watchpack": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
"integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
"integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
"dev": true,
"requires": {
"glob-to-regexp": "^0.4.1",
@@ -35681,9 +35664,9 @@
"dev": true
},
"webpack": {
"version": "5.69.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.69.1.tgz",
"integrity": "sha512-+VyvOSJXZMT2V5vLzOnDuMz5GxEqLk7hKWQ56YxPW/PQRUuKimPqmEIJOx8jHYeyo65pKbapbW464mvsKbaj4A==",
"version": "5.76.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz",
"integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==",
"dev": true,
"requires": {
"@types/eslint-scope": "^3.7.3",
@@ -35691,24 +35674,24 @@
"@webassemblyjs/ast": "1.11.1",
"@webassemblyjs/wasm-edit": "1.11.1",
"@webassemblyjs/wasm-parser": "1.11.1",
"acorn": "^8.4.1",
"acorn": "^8.7.1",
"acorn-import-assertions": "^1.7.6",
"browserslist": "^4.14.5",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.8.3",
"enhanced-resolve": "^5.10.0",
"es-module-lexer": "^0.9.0",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.2.9",
"json-parse-better-errors": "^1.0.2",
"json-parse-even-better-errors": "^2.3.1",
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
"schema-utils": "^3.1.0",
"tapable": "^2.1.1",
"terser-webpack-plugin": "^5.1.3",
"watchpack": "^2.3.1",
"watchpack": "^2.4.0",
"webpack-sources": "^3.2.3"
},
"dependencies": {

View File

@@ -69,9 +69,18 @@
"electron": "^21.1.0",
"electron-is-dev": "^2.0.0",
"generate-license-file": "^2.0.0",
"identity-obj-proxy": "^3.0.0",
"license-checker": "^25.0.1",
"react-scripts": "5.0.1",
"ts-node": "^10.9.1",
"wait-on": "^6.0.1"
},
"jest": {
"transformIgnorePatterns": [
"node_modules/(?!(@?react-leaflet|axios)/)"
],
"moduleNameMapper": {
".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "identity-obj-proxy"
}
}
}

View File

@@ -0,0 +1,53 @@
import Aircraft from "./Aircraft";
import { render } from "@testing-library/react";
import { Icon } from "leaflet";
const mockMarker = jest.fn();
jest.mock("react-leaflet", () => ({
Marker: (props: any) => {
mockMarker(props);
},
}));
test("grounded aircraft do not render", async () => {
const { container } = render(
<Aircraft
flight={{
id: "",
blue: true,
position: undefined,
sidc: "",
waypoints: [],
}}
/>
);
expect(container).toBeEmptyDOMElement();
});
test("in-flight aircraft render", async () => {
render(
<Aircraft
flight={{
id: "",
blue: true,
position: {
lat: 10,
lng: 20,
},
sidc: "foobar",
waypoints: [],
}}
/>
);
expect(mockMarker).toHaveBeenCalledWith(
expect.objectContaining({
position: {
lat: 10,
lng: 20,
},
icon: expect.any(Icon),
})
);
});

8
codecov.yaml Normal file
View File

@@ -0,0 +1,8 @@
coverage:
status:
patch:
default:
informational: false
project:
default:
informational: true

20
docs/Makefile Normal file
View File

@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

34
docs/conf.py Normal file
View File

@@ -0,0 +1,34 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "DCS Liberation"
copyright = "2023, DCS Liberation Team"
author = "DCS Liberation Team"
release = "7.0.0"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
"myst_parser",
"sphinx_rtd_theme",
"sphinx.ext.autosectionlabel",
"sphinx.ext.todo",
]
templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = "sphinx_rtd_theme"
html_static_path = ["_static"]
todo_include_todos = True

View File

@@ -0,0 +1,8 @@
Design docs
===========
.. toctree::
:maxdepth: 2
:caption: Contents:
turnless.md

8
docs/dev/index.rst Normal file
View File

@@ -0,0 +1,8 @@
Developer documentation
=======================
.. toctree::
:maxdepth: 2
:caption: Contents:
design/index.rst

6
docs/game/index.rst Normal file
View File

@@ -0,0 +1,6 @@
Manual
======
.. toctree::
:maxdepth: 2
:caption: Contents:

16
docs/index.rst Normal file
View File

@@ -0,0 +1,16 @@
DCS Liberation
==============
.. toctree::
:maxdepth: 2
:caption: Contents:
game/index.rst
modding/index.rst
dev/index.rst
Indices and tables
==================
* :ref:`genindex`
* :ref:`search`

35
docs/make.bat Normal file
View File

@@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

10
docs/modding/index.rst Normal file
View File

@@ -0,0 +1,10 @@
Modding guide
=============
.. toctree::
:maxdepth: 2
:caption: Contents:
fuel-consumption-measurement.md
layouts.rst
weather.rst

397
docs/modding/layouts.rst Normal file
View File

@@ -0,0 +1,397 @@
The Layout System
=================
.. note::
The documentation of the layout system is still WIP and not
complete as the development of this feature involves a major refactoring
of the base code. Therefore this documentation is currently used for
development purpose primarily. The documentation will be updated soon.
Any help in updating this wiki page is appreciated!
The Layout System is a new way of defining how ground objects like SAM
Sites or other Vehicle / Ship Groups will be generated (which type of
units, how many units, alignment and orientation). It is a complete
rework of the previous generator-based logic which was written in python
code. The new system allows to define layouts with easy to write yaml
code and the use of the DCS Mission Editor for easier placement of the
units. The layout system also introduced a new logical grouping of Units
and layouts for them, the Armed Forces, which will allow major
improvements to the Ground Warfare in upcoming features.
**Armed Forces**
The Armed Forces is a new system introduced with the layout system which will
allow to identitfy and group possible units from the faction together with
available layouts for these groups. It is comparable to the AirWing and Squadron
implementation but just for Ground and Naval Forces. All possible Force Groups
(grouping of 1 or more units and and the available layouts for them) will be
generated during campaign initialization and will be used later by many
different systems. A Force Group can also include static objects which was not
possible before the introduction of the layout system. It is also possible to
define presets of these Force Groups within the faction file which is handy for
more complex groups like a SA-10 Battery or similar. Example: `SA-10.yaml`_.
which includes all the units like SR, TR, LN and has the layout of a
`S-300_Site.yaml`_.
.. _SA-10.yaml: https://github.com/dcs-liberation/dcs_liberation/blob/develop/resources/groups/SA-10.yaml
.. _S-300_Site.yaml: https://github.com/dcs-liberation/dcs_liberation/blob/develop/resources/layouts/anti_air/S-300_Site.yaml
**The Layout System**
In the previous system the generator which created the ground object was written
in python which made modifications and reusability very complicated. To allow
easier handling of the layouts and decoupling of alignment of units and the
actual unit type (for example Ural-375) the layout system was introduced.
Previously we had a generator for every different SAM Site, now we can just
reuse the alignemnt (e.g. 6 Launchers in a circle alignment) for multiple SAM
Systems and introduce more variety.
This new System allows Users and Designers to easily create or modify
layouts as the new alginment and orientation of units is defined with
the DCS Mission editor. An additional .yaml file allows the
configuration of the layout with settings like allow unit types or
random amounts of units. In total the new system reduces the complexity
and allows to precisely align / orient units as needed and create
realistic looking ground units.
As the whole ground unit generation and handling was reworked it is now
also possible to add static units to a ground object, so even
Fortifcation or similar can be added to templates in the future.
General Concept
~~~~~~~~~~~~~~~
.. figure:: images/layouts.png
:alt: Overview
Overview
All possible Force Groups will be generated during campaign
initialization by checking all possible units for the specific faction
and all available layouts. The code will automatically match general
layouts with available units. It is also possible to define preset
groups within the faction files which group many units and the prefered
layouts for the group. This is especially handy for unique layouts which
are not defined as ``global``. For example complex sam sites like the
S-300 or Patriot which have very specific alignment of the different
units.
Layouts will be matched to units based on the special definition given
in the corresponding yaml file. For example a layout which is defined as
global and allows the unit_class SHORAD will automatically be used for
all available SHORAD units which are defined in the faction file.
.. todo:: Describe the optional flag.
All these generated ForceGroups will be managed by the ArmedForces class
of the specific coalition. This class will be used by other parts of the
system like the start_generator or the BuyMenu. The ArmedForces class
will then generate the TheaterGroundObject which will be used by
liberation.
Example for a customized Ground Object Buy Menu which makes use of
Templates and UnitGroups:
.. figure:: images/ground_object_buy_menu.png
:alt: Ground object buy menu
Ground object buy menu
How to modify or add layouts
----------------------------
.. warning::
Whenever changes were made to layouts they have to be re-imported into
Liberation. See :ref:`Import Layouts into Liberation`.
A layout consists of two special files:
- layout.miz which defines the actual positioning and alignment of the
groups / units
- layout.yaml which defines the necessary information like amount of
units, possible types or classes.
To add a new template a new yaml has to be created as every yaml can
only define exact one template. Best practice is to copy paste an
existing template which is similar to the one to be created as starting
point. The next step is to create a new .miz file and align Units and
statics to the wishes. Even if existing ones can be reused, best
practice is to always create a fresh one to prevent side effects. The
most important part is to use a new Group for every different Unit Type.
It is not possible to mix Unit Types in one group within a template. For
example it is not possible to have a logistic truck and a AAA in the
same group. The miz file will only be used to get the exact position and
orientation of the units, therefore it is irrelevant which type of unit
will be used. The unit type will be later defined inside the yaml file.
For the next step all Group names have to be added to the yaml file.
Take care to that these names match exactly! Assign the unit_types or
unit_classes properties to math the needs.
The Layout miz
~~~~~~~~~~~~~~
The miz file is used to define the positioning and orientation of the
different units for the template. The actual unit which is used is
irrelevant. It is important to use a unique and meaningful name for the
groups as this will be used in the yaml file as well. The information
which will be extracted from the miz file are just the coordinates and
heading of the units.
*Important*: Every different unit type has to be in a separate Group for
the template to work. You can not add units of different types to the
same group. They can get merged back together during generation by
setting the group property. In the example below both groups
``AAA Site 0`` and ``AAA Site 1`` have the group = 1 which means that
they will be in the same dcs group during generation.
*Important*: Liberation expects every template to be designed with an
orientation of heading 0 (North) in mind. The complete GroundObject will
during the campaign generation process be rotated to match the
orientation defined by the campaign designer. If the layout was not
created with an orientation of heading 0 the later generated
GroundObject will likely be misaligned and not work properly.
.. todo::
max amount of possible units is defined from the miz. Example if later the
group should have 6 units than there have to be 6 defined in the miz.
.. figure:: images/layout_miz_example.png
:alt: Example template mission
Example template mission
The Layout configuration file
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. todo:: Description about the layout yaml file.
Possible Information:
.. list-table::
:header-rows: 1
* - Property
- Type
- Required
- Description
- Example
* - name
- ``str``
- Yes
- A name to identify the template
- .. code:: yaml
name: Armor Group
* - tasks
- list of ``GroupTask``
- Yes
- A list of tasks which the template can fulfill
- .. code:: yaml
tasks:
- AAA
- SHORAD
* - generic
- ``bool``, default false
- No
- True if this template will be used to create general ``UnitGroups``
-
* - description
- ``str``
- No
- Short description of the template
-
* - groups
- List of ``Groups``
- Yes
- See below for definition of a group
-
* - layout_file
- ``str``
- No
- The .miz file which has the groups/units of the layout included. Only
needed if the file has a different name than the yaml file
- .. code:: yaml
layout_file: resources/layouts/naval/legacy_naval_templates.miz
.. todo:: Group and SubGroup
A group has 1..N sub groups. The name of the Group will be used later
within the DCS group name.
All SubGroups will be merged into one DCS Group
Every unit type has to be defined as a sub group as following:
.. list-table::
:header-rows: 1
* - Property
- Type
- Required
- Description
* - name
- ``str``
- Yes
- The group name used in the .miz. Must match exactly!
* - optional
- ``bool``, default: false
- No
- Defines wether the layout can be used without this group if the faction
has no access to the unit type or the user wants to disable this group
* - fill
- ``bool``, default: false
- No
- If the group is optional the layout is used from a PresetGroup this
property tells the system if it should use any possible faction
accessible unit to fill up this slot if no capable one was defined in
the preset yaml.
* - unit_count
- list of ``int``
- No
- Amount of units to be generated for this group. Can be fixed or a range
where it will be picked randomly
* - unit_types
- list of DCS unit type IDs
- No
- Specific unit_types for ground units. Complete list from `vehicles.py`_.
This list is extended by all supported mods!
* - unit_classes
- list of unit classes
- No
- Unit classes of supported units. Defined by ``UnitClass`` in
`game/data/units.py`_.
* - statics
- list of static types
- No
- Specific unit_types of statics. Complete list from `statics.py`_
.. _vehicles.py: https://github.com/pydcs/dcs/blob/master/dcs/vehicles.py
.. _game/data/units.py: https://github.com/dcs-liberation/dcs_liberation/blob/develop/game/data/units.py
.. _statics.py: https://github.com/pydcs/dcs/blob/master/dcs/statics.py
Complete example of a generic template for an Aircraft Carrier group:
.. code:: yaml
name: Carrier Group
generic: true
tasks:
- AircraftCarrier
groups:
- Carrier: # Group Name of the DCS Group
- name: Carrier Group 0 # Sub Group used in the layout.miz
unit_count:
- 1
unit_classes:
- AircraftCarrier
- Escort: # Group name of the 2nd Group
- name: Carrier Group 1
unit_count:
- 4
unit_classes:
- Destroyer
layout_file: resources/layouts/naval/legacy_naval_templates.miz
Import Layouts into Liberation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For performance improvements all layouts are serialized to a so called
pickle file inside the save folder defined in the liberation
preferences. Every time changes are made to the layouts this file has to
be recreated. It can be recreated by either deleting the layouts.p file
manually or using the special option in the Liberation Toolbar
(Developer Tools -> Import Layouts). It will also be recreated after
each Liberation update as it will check the Version Number and recreate
it when changes are recognized.
Migration from Generators
-------------------------
The previous generators were migrated using a script which build a group using
the generator. All of these groups were save into one .miz file
`original_generator_layouts.miz`_. This miz file can be used to verify the
templates and to generalize similar templates to decouple the layout from the
actual units. As this is a time-consuming and sphisticated task this will be
done over time. With the first step the technical requirements will be fulfilled
so that the generalization can happen afterwards the technical pr gets merged.
.. _original_generator_layouts.miz: https://github.com/dcs-liberation/dcs_liberation/blob/develop/resources/layouts/original_generator_layouts.miz
Updates for Factions
~~~~~~~~~~~~~~~~~~~~
With the rework there were also some changes to the faction file
definitions. Older faction files can not be loaded anymore and have to
be adopted to the new changes. During migration all default factions
were automatically updated, so they will work out of the box.
You can find more detailed information about how to customize the
faction file in `Custom factions`_.
What was changed:
* Removed the ``ewrs`` list. All EWRs are now defined in the list
``air_defense_units``.
* Added the ``air_defense_units`` list. All units with the Role AntiAir can be
defined here as `GroundUnitType`_. All possible units are defined in
`resources/units/ground_units`_.
* Added ``preset_groups``. This list allows to define Preset Groups (described
above) like SAM Systems consisting of Launcher, SR, TR and so on instead of
adding them each to “air_defense_units”. The presets are defined in
`resources/groups`_
* Migrated ``air_defenses`` to air_defense_units and preset_sets.
* ``Missiles`` are migrated to GroundUnitTypes instead of Generator names (see
air_defense_units for how to use)
* Removed ``cruisers``, ``destroyers`` and ``naval_generators``. Migrated them
to naval_units and preset_groups
* Added ``naval_units`` with the correct ship name found in
`resources/units/ships`_.
* ``aircraft_carrier`` and ``helicopter_carrier`` were moved to ``naval_units``
as well.
.. _Custom factions: https://github.com/dcs-liberation/dcs_liberation/wiki/Custom-Factions
.. _GroundUnitType: https://github.com/dcs-liberation/dcs_liberation/blob/develop/game/dcs/groundunittype.py
.. _resources/units/ground_units: https://github.com/dcs-liberation/dcs_liberation/blob/develop/resources/units/ground_units
.. _resources/units/ships: https://github.com/dcs-liberation/dcs_liberation/blob/develop/resources/units/ships
.. _resources/groups: https://github.com/dcs-liberation/dcs_liberation/blob/develop/resources/groups
Preset Groups
-------------
Instead of adding the exact name of the previous generator to add
complex groups like SAM sites or similar to the faction it is now
possible to add preset groups to the faction file. As described earlier
such a preset group (Force Group) can be defined very easy with a yaml
file. This file allows to define the name, tasking, units, statics and
the prefered layouts. The first task defines the primary role of the
ForceGroup which gets generated from the preset.
Example:
.. code:: yaml
name: SA-10/S-300PS # The name of the group
tasks: # Define at least 1 task
- LORAD # The task(s) the Group can fulfill
units: # Define at least 1 unit
- SAM SA-10 S-300 "Grumble" Clam Shell SR
- SAM SA-10 S-300 "Grumble" Big Bird SR
- SAM SA-10 S-300 "Grumble" C2
- SAM SA-10 S-300 "Grumble" Flap Lid TR
- SAM SA-10 S-300 "Grumble" TEL D
- SAM SA-10 S-300 "Grumble" TEL C
statics: # Optional
- # Add some statics here
layouts: # Define at least one layout
- S-300 Site # prefered layouts for these groups
Resources:
* A list of all available preset groups can be found here: `resources/groups`_
* All possible tasks can be found in the `game/data/groups.py`_
* Units are defined with the variant name found in `resources/units`_
.. _game/data/groups.py: https://github.com/dcs-liberation/dcs_liberation/blob/develop/game/data/groups.py
.. _resources/units: https://github.com/dcs-liberation/dcs_liberation/tree/develop/resources/units

76
docs/modding/weather.rst Normal file
View File

@@ -0,0 +1,76 @@
#######
Weather
#######
Weather conditions in DCS Liberation are randomly generated at the start of each
turn. Some of the inputs to that generator (more to come) can be controlled via
the config files in ``resources/weather``.
**********
Archetypes
**********
A weather archetype defines the the conditions for a style of weather, such as
"clear", or "raining". There are currently four archetypes:
1. clear
2. cloudy
3. raining
4. thunderstorm
The odds of each archetype appearing in each season are defined in the theater
yaml files (``resources/theaters/*/info.yaml``).
.. literalinclude:: ../../resources/weather/archetypes/clear.yaml
:language: yaml
:linenos:
:caption: resources/weather/archetypes/clear.yaml
Wind speeds
===========
DCS missions define wind with a speed and heading at each of three altitudes:
1. MSL
2. 2000 meters
3. 8000 meters
Blending between each altitude band is done in a manner defined by DCS.
Liberation randomly generates a direction for the wind at MSL, and each other
altitude band will have wind within +/- 90 degrees of that heading.
Wind speeds can be modded by altering the ``speed`` dict in the archetype yaml.
The only random distribution currently supported is the Weibull distribution, so
all archetypes currently use:
.. code:: yaml
speed:
weibull:
...
The Weibull distribution has two parameters: a shape and a scale.
The scale is simplest to understand. 63.2% of all outcomes of the distribution
are below the scale parameter.
The shape controls where the peak of the distribution is. See the examples in
the links below for illustrations and guidelines, but generally speaking low
values (between 1 and 2.6) will cause low speeds to be more common, medium
values (around 3) will be fairly evenly distributed around the median, and high
values (greater than 3.7) will cause high speeds to be more common. As wind
speeds tend to be higher at higher altitudes and fairly slow close to the
ground, you typically want a low value for MSL, a medium value for 2000m, and a
high value for 8000m.
For examples, see https://statisticsbyjim.com/probability/weibull-distribution/.
To experiment with different inputs, use Wolfram Alpha, e.g.
https://www.wolframalpha.com/input?i=weibull+distribution+1.5+5.
When generating wind speeds, each subsequent altitude band will have the lower
band's speed added to its scale parameter. That is, for the example above, the
actual scale parameter of ``at_2000m`` will be ``20 + wind speed at MSL``, and
the scale parameter of ``at_8000m`` will be ``20 + wind speed at 2000m``. This
is to ensure that a generally windy day (high wind speed at MSL) will create
similarly high winds at higher altitudes and vice versa.

2
docs/requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
myst-parser
sphinx_rtd_theme

View File

@@ -1,627 +0,0 @@
import logging
from collections.abc import Sequence
from typing import Type
from dcs.helicopters import (
AH_1W,
AH_64A,
AH_64D,
AH_64D_BLK_II,
CH_47D,
CH_53E,
Ka_50,
Mi_24P,
Mi_24V,
Mi_26,
Mi_28N,
Mi_8MT,
OH_58D,
SA342L,
SA342M,
SH_60B,
UH_1H,
UH_60A,
)
from dcs.planes import (
AJS37,
AV8BNA,
A_10A,
A_10C,
A_10C_2,
A_20G,
A_50,
An_26B,
B_17G,
B_1B,
B_52H,
Bf_109K_4,
C_101CC,
C_130,
C_17A,
E_2C,
E_3A,
FA_18C_hornet,
FW_190A8,
FW_190D9,
F_117A,
F_14A_135_GR,
F_14B,
F_15C,
F_15E,
F_16A,
F_16C_50,
F_4E,
F_5E_3,
F_86F_Sabre,
H_6J,
IL_76MD,
IL_78M,
I_16,
JF_17,
J_11A,
Ju_88A4,
KC130,
KC135MPRS,
KC_135,
KJ_2000,
L_39ZA,
MB_339A,
MQ_9_Reaper,
M_2000C,
MiG_15bis,
MiG_19P,
MiG_21Bis,
MiG_23MLD,
MiG_25PD,
MiG_27K,
MiG_29A,
MiG_29G,
MiG_29S,
MiG_31,
Mirage_2000_5,
Mirage_F1B,
Mirage_F1BE,
Mirage_F1CE,
Mirage_F1CT,
Mirage_F1C_200,
Mirage_F1EE,
Mirage_F1EQ,
Mirage_F1M_CE,
Mirage_F1M_EE,
MosquitoFBMkVI,
P_47D_30,
P_47D_30bl1,
P_47D_40,
P_51D,
P_51D_30_NA,
RQ_1A_Predator,
S_3B,
S_3B_Tanker,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
Su_17M4,
Su_24M,
Su_25,
Su_25T,
Su_25TM,
Su_27,
Su_30,
Su_33,
Su_34,
Tornado_GR4,
Tornado_IDS,
Tu_142,
Tu_160,
Tu_22M3,
Tu_95MS,
WingLoong_I,
Yak_40,
)
from dcs.unittype import FlyingType
from game.dcs.aircrafttype import AircraftType
from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.f104.f104 import VSN_F104G, VSN_F104S, VSN_F104S_AG
from pydcs_extensions.f22a.f22a import F_22A
from pydcs_extensions.hercules.hercules import Hercules
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
from pydcs_extensions.ov10a.ov10a import Bronco_OV_10A
from pydcs_extensions.su57.su57 import Su_57
from pydcs_extensions.uh60l.uh60l import KC130J, UH_60L
from .flighttype import FlightType
# All aircraft lists are in priority order. Aircraft higher in the list will be
# preferred over those lower in the list.
# 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.
# Used for CAP, Escort, and intercept if there is not a specialised aircraft available
CAP_CAPABLE = [
Su_57,
F_22A,
F_15C,
F_14B,
F_14A_135_GR,
Su_33,
J_11A,
Su_30,
Su_27,
MiG_29S,
F_16C_50,
FA_18C_hornet,
JF_17,
JAS39Gripen,
F_16A,
F_4E,
MiG_31,
MiG_25PD,
MiG_29G,
MiG_29A,
MiG_23MLD,
MiG_21Bis,
Mirage_2000_5,
Mirage_F1B,
Mirage_F1BE,
Mirage_F1CE,
Mirage_F1EE,
Mirage_F1EQ,
Mirage_F1M_CE,
Mirage_F1M_EE,
Mirage_F1C_200,
Mirage_F1CT,
F_15E,
M_2000C,
F_5E_3,
VSN_F104S,
VSN_F104G,
MiG_19P,
A_4E_C,
F_86F_Sabre,
MiG_15bis,
C_101CC,
L_39ZA,
P_51D_30_NA,
P_51D,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
MosquitoFBMkVI,
Bf_109K_4,
FW_190D9,
FW_190A8,
P_47D_30,
P_47D_30bl1,
P_47D_40,
I_16,
]
# Used for CAS (Close air support) and BAI (Battlefield Interdiction)
CAS_CAPABLE = [
A_10C_2,
A_10C,
Hercules,
Su_34,
Su_25TM,
Su_25T,
Su_25,
F_15E,
F_16C_50,
FA_18C_hornet,
Tornado_GR4,
Tornado_IDS,
JAS39Gripen_AG,
JF_17,
AV8BNA,
A_10A,
B_1B,
A_4E_C,
Bronco_OV_10A,
F_14B,
F_14A_135_GR,
AJS37,
Su_24M,
Su_17M4,
Su_33,
F_4E,
S_3B,
Su_30,
MiG_29S,
MiG_27K,
MiG_29A,
MiG_21Bis,
AH_64D_BLK_II,
AH_64D,
AH_64A,
AH_1W,
OH_58D,
SA342M,
SA342L,
Ka_50,
Mi_28N,
Mi_24P,
Mi_24V,
Mi_8MT,
H_6J,
MiG_19P,
MiG_15bis,
M_2000C,
Mirage_F1B,
Mirage_F1BE,
Mirage_F1CE,
Mirage_F1EE,
Mirage_F1EQ,
Mirage_F1M_CE,
Mirage_F1M_EE,
Mirage_F1CT,
F_5E_3,
F_86F_Sabre,
MB_339A,
C_101CC,
L_39ZA,
UH_1H,
VSN_F104S_AG,
VSN_F104G,
A_20G,
Ju_88A4,
P_47D_40,
P_47D_30bl1,
P_47D_30,
P_51D_30_NA,
P_51D,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
MosquitoFBMkVI,
I_16,
Bf_109K_4,
FW_190D9,
FW_190A8,
WingLoong_I,
MQ_9_Reaper,
RQ_1A_Predator,
]
# Aircraft used for SEAD and SEAD Escort tasks. Must be capable of the CAS DCS task.
SEAD_CAPABLE = [
JF_17,
F_16C_50,
FA_18C_hornet,
Tornado_IDS,
Su_25T,
Su_25TM,
F_4E,
A_4E_C,
F_14B,
F_14A_135_GR,
JAS39Gripen_AG,
AV8BNA,
Su_24M,
Su_17M4,
Su_34,
Su_30,
MiG_27K,
Tornado_GR4,
]
# Aircraft used for DEAD tasks. Must be capable of the CAS DCS task.
DEAD_CAPABLE = SEAD_CAPABLE + [
AJS37,
F_14B,
F_14A_135_GR,
JAS39Gripen_AG,
B_1B,
B_52H,
Tu_160,
Tu_95MS,
H_6J,
A_20G,
Ju_88A4,
VSN_F104S_AG,
VSN_F104G,
P_47D_40,
P_47D_30bl1,
P_47D_30,
P_51D_30_NA,
P_51D,
Bronco_OV_10A,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
MosquitoFBMkVI,
Bf_109K_4,
FW_190D9,
FW_190A8,
]
# Aircraft used for Strike mission
STRIKE_CAPABLE = [
F_117A,
B_1B,
B_52H,
Tu_160,
Tu_95MS,
Tu_22M3,
H_6J,
F_15E,
AJS37,
Tornado_GR4,
F_16C_50,
FA_18C_hornet,
AV8BNA,
JF_17,
F_16A,
F_14B,
F_14A_135_GR,
JAS39Gripen_AG,
Tornado_IDS,
Su_17M4,
Su_24M,
Su_25TM,
Su_25T,
Su_25,
Su_34,
Su_33,
Su_30,
Su_27,
MiG_29S,
MiG_29G,
MiG_29A,
F_4E,
A_10C_2,
A_10C,
S_3B,
A_4E_C,
Bronco_OV_10A,
M_2000C,
Mirage_F1B,
Mirage_F1BE,
Mirage_F1CE,
Mirage_F1EE,
Mirage_F1EQ,
Mirage_F1M_CE,
Mirage_F1M_EE,
Mirage_F1CT,
MiG_27K,
MiG_21Bis,
MiG_15bis,
F_5E_3,
F_86F_Sabre,
MB_339A,
C_101CC,
L_39ZA,
B_17G,
A_20G,
Ju_88A4,
VSN_F104S_AG,
VSN_F104G,
P_47D_40,
P_47D_30bl1,
P_47D_30,
P_51D_30_NA,
P_51D,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
MosquitoFBMkVI,
Bf_109K_4,
FW_190D9,
FW_190A8,
]
ANTISHIP_CAPABLE = [
AJS37,
Tu_142,
Tu_22M3,
H_6J,
FA_18C_hornet,
JAS39Gripen_AG,
Su_24M,
Su_17M4,
JF_17,
Su_34,
Su_30,
Tornado_IDS,
Tornado_GR4,
AV8BNA,
S_3B,
A_20G,
Ju_88A4,
MosquitoFBMkVI,
C_101CC,
SH_60B,
]
# This list does not "inherit" from the strike list because some strike aircraft can
# only carry guided weapons, and the AI cannot do runway attack with dguided weapons.
# https://github.com/dcs-liberation/dcs_liberation/issues/1703
RUNWAY_ATTACK_CAPABLE = [
JF_17,
Su_34,
Su_30,
Tornado_IDS,
M_2000C,
H_6J,
B_1B,
B_52H,
Tu_22M3,
H_6J,
F_15E,
AJS37,
F_16C_50,
FA_18C_hornet,
AV8BNA,
JF_17,
F_16A,
F_14B,
F_14A_135_GR,
JAS39Gripen_AG,
Tornado_IDS,
Su_17M4,
Su_24M,
Su_25TM,
Su_25T,
Su_25,
Su_34,
Su_33,
Su_30,
Su_27,
MiG_29S,
MiG_29G,
MiG_29A,
F_4E,
A_10C_2,
A_10C,
S_3B,
A_4E_C,
Bronco_OV_10A,
M_2000C,
Mirage_F1B,
Mirage_F1BE,
Mirage_F1CE,
Mirage_F1EE,
Mirage_F1EQ,
Mirage_F1M_CE,
Mirage_F1M_EE,
Mirage_F1CT,
MiG_27K,
MiG_21Bis,
MiG_15bis,
MB_339A,
F_5E_3,
F_86F_Sabre,
C_101CC,
L_39ZA,
B_17G,
A_20G,
Ju_88A4,
VSN_F104S_AG,
VSN_F104G,
P_47D_40,
P_47D_30bl1,
P_47D_30,
P_51D_30_NA,
P_51D,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
MosquitoFBMkVI,
Bf_109K_4,
FW_190D9,
FW_190A8,
]
# For any aircraft that isn't necessarily directly involved in strike
# missions in a direct combat sense, but can transport objects and infantry.
TRANSPORT_CAPABLE = [
C_17A,
Hercules,
C_130,
IL_76MD,
An_26B,
Yak_40,
CH_53E,
CH_47D,
UH_60L,
SH_60B,
UH_60A,
UH_1H,
Mi_8MT,
Mi_8MT,
Mi_26,
]
AIR_ASSAULT_CAPABLE = [
CH_53E,
CH_47D,
UH_60L,
SH_60B,
UH_60A,
UH_1H,
Mi_8MT,
Mi_26,
Mi_24P,
Mi_24V,
Hercules,
]
DRONES = [MQ_9_Reaper, RQ_1A_Predator, WingLoong_I]
AEWC_CAPABLE = [
E_3A,
E_2C,
A_50,
KJ_2000,
]
# Priority is given to the tankers that can carry the most fuel.
REFUELING_CAPABALE = [
KC_135,
KC135MPRS,
IL_78M,
KC130J,
KC130,
S_3B_Tanker,
]
def dcs_types_for_task(task: FlightType) -> Sequence[Type[FlyingType]]:
cap_missions = (
FlightType.BARCAP,
FlightType.INTERCEPTION,
FlightType.SWEEP,
FlightType.TARCAP,
)
if task in cap_missions:
return CAP_CAPABLE
elif task == FlightType.ANTISHIP:
return ANTISHIP_CAPABLE
elif task == FlightType.BAI:
return CAS_CAPABLE
elif task == FlightType.CAS:
return CAS_CAPABLE
elif task == FlightType.SEAD:
return SEAD_CAPABLE
elif task == FlightType.SEAD_ESCORT:
return SEAD_CAPABLE
elif task == FlightType.DEAD:
return DEAD_CAPABLE
elif task == FlightType.OCA_AIRCRAFT:
return CAS_CAPABLE
elif task == FlightType.OCA_RUNWAY:
return RUNWAY_ATTACK_CAPABLE
elif task == FlightType.STRIKE:
return STRIKE_CAPABLE
elif task == FlightType.ESCORT:
return CAP_CAPABLE
elif task == FlightType.AEWC:
return AEWC_CAPABLE
elif task == FlightType.REFUELING:
return REFUELING_CAPABALE
elif task == FlightType.TRANSPORT:
return TRANSPORT_CAPABLE
elif task == FlightType.AIR_ASSAULT:
return AIR_ASSAULT_CAPABLE
else:
logging.error(f"Unplannable flight type: {task}")
return []
def aircraft_for_task(task: FlightType) -> list[AircraftType]:
dcs_types = dcs_types_for_task(task)
types: list[AircraftType] = []
for dcs_type in dcs_types:
types.extend(AircraftType.for_dcs_type(dcs_type))
return types
def tasks_for_aircraft(aircraft: AircraftType) -> list[FlightType]:
tasks: list[FlightType] = []
for task in FlightType:
if task is FlightType.FERRY:
# Not a plannable task, so skip it.
continue
if aircraft in aircraft_for_task(task):
tasks.append(task)
return tasks

View File

@@ -171,6 +171,15 @@ class Flight(SidcDescribable):
def missing_pilots(self) -> int:
return self.roster.missing_pilots
def set_flight_type(self, var: FlightType) -> None:
self.flight_type = var
# Update _flight_plan_builder so that the builder class remains relevant
# to the flight type
from .flightplans.flightplanbuildertypes import FlightPlanBuilderTypes
self._flight_plan_builder = FlightPlanBuilderTypes.for_flight(self)(self)
def return_pilots_and_aircraft(self) -> None:
self.roster.clear()
self.squadron.claim_inventory(-self.count)

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from datetime import datetime
from typing import Iterator, TYPE_CHECKING, Type
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
@@ -55,12 +55,12 @@ class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
def tot_waypoint(self) -> FlightWaypoint:
return self.layout.drop_off
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.tot_waypoint:
return self.tot
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
return None
@property
@@ -68,7 +68,11 @@ class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
return meters(2500)
@property
def mission_departure_time(self) -> timedelta:
def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> datetime:
return self.package.time_over_target
def ui_zone(self) -> UiZone:

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from datetime import datetime
from typing import TYPE_CHECKING, Type
from game.theater.missiontarget import MissionTarget
@@ -67,16 +67,20 @@ class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
# drop-off waypoint.
return self.layout.drop_off or self.layout.arrival
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
# TOT planning isn't really useful for transports. They're behind the front
# lines so no need to wait for escorts or for other missions to complete.
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
return None
@property
def mission_departure_time(self) -> timedelta:
def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> datetime:
return self.package.time_over_target

View File

@@ -29,7 +29,7 @@ class CasLayout(PatrollingLayout):
yield self.target
yield self.patrol_end
yield from self.nav_from
yield self.departure
yield self.arrival
if self.divert is not None:
yield self.divert
yield self.bullseye

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from datetime import datetime
from typing import TYPE_CHECKING, Type
from .flightplan import FlightPlan, Layout
@@ -42,16 +42,20 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
return waypoint
return self.layout.departure
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.tot_waypoint:
return self.package.time_over_target
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
return None
@property
def mission_departure_time(self) -> timedelta:
def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> datetime:
return self.package.time_over_target

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from datetime import datetime
from typing import TYPE_CHECKING, Type
from game.utils import feet
@@ -37,16 +37,20 @@ class FerryFlightPlan(StandardFlightPlan[FerryLayout]):
def tot_waypoint(self) -> FlightWaypoint:
return self.layout.arrival
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
# TOT planning isn't really useful for ferries. They're behind the front
# lines so no need to wait for escorts or for other missions to complete.
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
return None
@property
def mission_departure_time(self) -> timedelta:
def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> datetime:
return self.package.time_over_target

View File

@@ -8,10 +8,10 @@ generating the waypoints for the mission.
from __future__ import annotations
import math
from abc import ABC
from abc import ABC, abstractmethod
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from datetime import datetime, timedelta
from functools import cached_property
from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar
@@ -149,7 +149,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
raise NotImplementedError
@property
def tot(self) -> timedelta:
def tot(self) -> datetime:
return self.package.time_over_target + self.tot_offset
@cached_property
@@ -215,7 +215,13 @@ class FlightPlan(ABC, Generic[LayoutT]):
for previous_waypoint, waypoint in self.edges(until=destination):
total += self.travel_time_between_waypoints(previous_waypoint, waypoint)
return total
# Trim microseconds. Our simulation tick rate is 1 second, so anything that
# takes 100.1 or 100.9 seconds will take 100 seconds. DCS doesn't handle
# sub-second resolution for tasks anyway, nor are they interesting from a
# mission planning perspective, so there's little value to keeping them in the
# model.
return timedelta(seconds=math.floor(total.total_seconds()))
def travel_time_between_waypoints(
self, a: FlightWaypoint, b: FlightWaypoint
@@ -224,10 +230,10 @@ class FlightPlan(ABC, Generic[LayoutT]):
a.position, b.position, self.speed_between_waypoints(a, b)
)
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
raise NotImplementedError
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
raise NotImplementedError
def request_escort_at(self) -> FlightWaypoint | None:
@@ -250,34 +256,20 @@ class FlightPlan(ABC, Generic[LayoutT]):
if waypoint == end:
return
def takeoff_time(self) -> timedelta:
def takeoff_time(self) -> datetime:
return self.tot - self._travel_time_to_waypoint(self.tot_waypoint)
def startup_time(self) -> timedelta:
start_time = (
self.takeoff_time() - self.estimate_startup() - self.estimate_ground_ops()
def minimum_duration_from_start_to_tot(self) -> timedelta:
return (
self._travel_time_to_waypoint(self.tot_waypoint)
+ self.estimate_startup()
+ self.estimate_ground_ops()
)
# In case FP math has given us some barely below zero time, round to
# zero.
if math.isclose(start_time.total_seconds(), 0):
start_time = timedelta()
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
# and they're not interesting from a mission planning perspective so we
# don't want them in the UI.
#
# Round down so *barely* above zero start times are just zero.
start_time = timedelta(seconds=math.floor(start_time.total_seconds()))
# Feature request #1309: Carrier planes should start at +1s
# This is a workaround to a DCS problem: some AI planes spawn on
# the 'sixpack' when start_time is zero and cause a deadlock.
# Workaround: force the start_time to 1 second for these planes.
if self.flight.from_cp.is_fleet and start_time.total_seconds() == 0:
start_time = timedelta(seconds=1)
return start_time
def startup_time(self) -> datetime:
return (
self.takeoff_time() - self.estimate_startup() - self.estimate_ground_ops()
)
def estimate_startup(self) -> timedelta:
if self.flight.start_type is StartType.COLD:
@@ -297,7 +289,17 @@ class FlightPlan(ABC, Generic[LayoutT]):
return timedelta(minutes=8)
@property
def mission_departure_time(self) -> timedelta:
@abstractmethod
def mission_begin_on_station_time(self) -> datetime | None:
"""The time that the mission is first on-station.
Not all mission types will have a time when they can be considered on-station.
Missions that patrol or loiter (CAPs, CAS, refueling, AEW&C, etc) will have this
defined, but strike-like missions will not.
"""
@property
def mission_departure_time(self) -> datetime:
"""The time that the mission is complete and the flight RTBs."""
raise NotImplementedError

View File

@@ -3,6 +3,8 @@ from __future__ import annotations
from typing import Any, TYPE_CHECKING, Type
from game.ato import FlightType
from game.theater.controlpoint import NavalControlPoint
from game.theater.frontline import FrontLine
from .aewc import AewcFlightPlan
from .airassault import AirAssaultFlightPlan
from .airlift import AirliftFlightPlan
@@ -19,6 +21,7 @@ from .ocarunway import OcaRunwayFlightPlan
from .packagerefueling import PackageRefuelingFlightPlan
from .planningerror import PlanningError
from .sead import SeadFlightPlan
from .shiprecoverytanker import RecoveryTankerFlightPlan
from .strike import StrikeFlightPlan
from .sweep import SweepFlightPlan
from .tarcap import TarCapFlightPlan
@@ -26,15 +29,19 @@ from .theaterrefueling import TheaterRefuelingFlightPlan
if TYPE_CHECKING:
from game.ato import Flight
from game.theater import FrontLine
class FlightPlanBuilderTypes:
@staticmethod
def for_flight(flight: Flight) -> Type[IBuilder[Any, Any]]:
if flight.flight_type is FlightType.REFUELING:
if flight.package.target.is_friendly(flight.squadron.player) or isinstance(
flight.package.target, FrontLine
target = flight.package.target
if target.is_friendly(flight.squadron.player) and isinstance(
target, NavalControlPoint
):
return RecoveryTankerFlightPlan.builder_type()
if target.is_friendly(flight.squadron.player) or isinstance(
target, FrontLine
):
return TheaterRefuelingFlightPlan.builder_type()
return PackageRefuelingFlightPlan.builder_type()

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import timedelta
from datetime import datetime, timedelta
from functools import cached_property
from typing import Any, TYPE_CHECKING, TypeGuard
@@ -73,15 +73,15 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
@property
@abstractmethod
def join_time(self) -> timedelta:
def join_time(self) -> datetime:
...
@property
@abstractmethod
def split_time(self) -> timedelta:
def split_time(self) -> datetime:
...
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.join:
return self.join_time
elif waypoint == self.layout.split:
@@ -89,7 +89,7 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
return None
@property
def push_time(self) -> timedelta:
def push_time(self) -> datetime:
return self.join_time - TravelTime.between_points(
self.layout.hold.position,
self.layout.join.position,
@@ -97,7 +97,11 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
)
@property
def mission_departure_time(self) -> timedelta:
def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> datetime:
return self.split_time
@self_type_guard

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from abc import ABC
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, TypeVar
from dcs import Point
@@ -91,14 +91,14 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
return total
@property
def join_time(self) -> timedelta:
def join_time(self) -> datetime:
travel_time = self.travel_time_between_waypoints(
self.layout.join, self.layout.ingress
)
return self.ingress_time - travel_time
@property
def split_time(self) -> timedelta:
def split_time(self) -> datetime:
travel_time_ingress = self.travel_time_between_waypoints(
self.layout.ingress, self.target_area_waypoint
)
@@ -115,14 +115,14 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
)
@property
def ingress_time(self) -> timedelta:
def ingress_time(self) -> datetime:
tot = self.tot
travel_time = self.travel_time_between_waypoints(
self.layout.ingress, self.target_area_waypoint
)
return tot - travel_time
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.ingress:
return self.ingress_time
elif waypoint in self.layout.targets:

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import timedelta
from datetime import datetime, timedelta
from typing import Any, TYPE_CHECKING, TypeGuard
from game.typeguard import self_type_guard
@@ -25,10 +25,10 @@ class LoiterFlightPlan(StandardFlightPlan[Any], ABC):
@property
@abstractmethod
def push_time(self) -> timedelta:
def push_time(self) -> datetime:
...
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.hold:
return self.push_time
return None

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from datetime import timedelta
from datetime import datetime, timedelta
from typing import Type
from dcs import Point
@@ -39,7 +39,7 @@ class PackageRefuelingFlightPlan(RefuelingFlightPlan):
)
@property
def patrol_start_time(self) -> timedelta:
def patrol_start_time(self) -> datetime:
altitude = self.flight.unit_type.patrol_altitude
if altitude is None:

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from datetime import datetime, timedelta
from typing import Any, TYPE_CHECKING, TypeGuard, TypeVar
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
@@ -61,22 +61,22 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
"""
@property
def patrol_start_time(self) -> timedelta:
def patrol_start_time(self) -> datetime:
return self.package.time_over_target
@property
def patrol_end_time(self) -> timedelta:
def patrol_end_time(self) -> datetime:
# TODO: This is currently wrong for CAS.
# CAS missions end when they're winchester or bingo. We need to
# configure push tasks for the escorts rather than relying on timing.
return self.patrol_start_time + self.patrol_duration
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.patrol_start:
return self.patrol_start_time
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.patrol_end:
return self.patrol_end_time
return None
@@ -90,7 +90,11 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
return self.layout.patrol_start
@property
def mission_departure_time(self) -> timedelta:
def mission_begin_on_station_time(self) -> datetime:
return self.patrol_start_time
@property
def mission_departure_time(self) -> datetime:
return self.patrol_end_time
@self_type_guard

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from datetime import datetime
from typing import TYPE_CHECKING, Type
from game.utils import feet
@@ -43,15 +43,19 @@ class RtbFlightPlan(StandardFlightPlan[RtbLayout]):
def tot_waypoint(self) -> FlightWaypoint:
return self.layout.abort_location
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
return None
@property
def mission_departure_time(self) -> timedelta:
return timedelta()
def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> datetime:
return self.tot
class Builder(IBuilder[RtbFlightPlan, RtbLayout]):

View File

@@ -0,0 +1,95 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Iterator, Type
from game.ato.flightplans.ibuilder import IBuilder
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
from game.ato.flightplans.waypointbuilder import WaypointBuilder
from game.ato.flightwaypoint import FlightWaypoint
@dataclass(frozen=True)
class RecoveryTankerLayout(StandardLayout):
nav_to: list[FlightWaypoint]
recovery_ship: FlightWaypoint
nav_from: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.nav_to
yield self.recovery_ship
yield from self.nav_from
yield self.arrival
if self.divert is not None:
yield self.divert
yield self.bullseye
class RecoveryTankerFlightPlan(StandardFlightPlan[RecoveryTankerLayout]):
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@property
def tot_waypoint(self) -> FlightWaypoint:
return self.layout.recovery_ship
@property
def mission_begin_on_station_time(self) -> datetime:
return self.package.time_over_target
@property
def mission_departure_time(self) -> datetime:
return self.patrol_end_time
@property
def patrol_start_time(self) -> datetime:
return self.package.time_over_target
@property
def patrol_end_time(self) -> datetime:
return self.tot + timedelta(hours=2)
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.tot_waypoint:
return self.tot
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.tot_waypoint:
return self.mission_departure_time
return None
class Builder(IBuilder[RecoveryTankerFlightPlan, RecoveryTankerLayout]):
def layout(self) -> RecoveryTankerLayout:
builder = WaypointBuilder(self.flight, self.coalition)
# TODO: Propagate the ship position to the Tanker's TOT,
# so that we minimize the tanker's need to catch up to the carrier.
recovery_ship = self.package.target.position
recovery_tanker = builder.recovery_tanker(recovery_ship)
# We don't have per aircraft cruise altitudes, so just reuse patrol altitude?
tanker_type = self.flight.unit_type
nav_cruise_altitude = tanker_type.preferred_patrol_altitude
return RecoveryTankerLayout(
departure=builder.takeoff(self.flight.departure),
nav_to=builder.nav_path(
self.flight.departure.position, recovery_ship, nav_cruise_altitude
),
nav_from=builder.nav_path(
recovery_ship, self.flight.arrival.position, nav_cruise_altitude
),
recovery_ship=recovery_tanker,
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
)
def build(self) -> RecoveryTankerFlightPlan:
return RecoveryTankerFlightPlan(self.flight, self.layout())

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from datetime import datetime, timedelta
from typing import Iterator, TYPE_CHECKING, Type
from dcs import Point
@@ -59,37 +59,42 @@ class SweepFlightPlan(LoiterFlightPlan):
return -self.lead_time
@property
def sweep_start_time(self) -> timedelta:
def sweep_start_time(self) -> datetime:
travel_time = self.travel_time_between_waypoints(
self.layout.sweep_start, self.layout.sweep_end
)
return self.sweep_end_time - travel_time
@property
def sweep_end_time(self) -> timedelta:
def sweep_end_time(self) -> datetime:
return self.tot
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.sweep_start:
return self.sweep_start_time
if waypoint == self.layout.sweep_end:
return self.sweep_end_time
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.hold:
return self.push_time
return None
@property
def push_time(self) -> timedelta:
def push_time(self) -> datetime:
return self.sweep_end_time - TravelTime.between_points(
self.layout.hold.position,
self.layout.sweep_end.position,
GroundSpeed.for_flight(self.flight, self.layout.hold.alt),
)
def mission_departure_time(self) -> timedelta:
@property
def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> datetime:
return self.sweep_end_time

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import random
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Type
from game.utils import Distance, Speed, feet
@@ -68,20 +68,20 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
def tot_offset(self) -> timedelta:
return -self.lead_time
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.patrol_end:
return self.patrol_end_time
return super().depart_time_for_waypoint(waypoint)
@property
def patrol_start_time(self) -> timedelta:
def patrol_start_time(self) -> datetime:
start = self.package.escort_start_time
if start is not None:
return start + self.tot_offset
return self.tot
@property
def patrol_end_time(self) -> timedelta:
def patrol_end_time(self) -> datetime:
end = self.package.escort_end_time
if end is not None:
return end

View File

@@ -23,7 +23,7 @@ from game.theater import (
TheaterGroundObject,
TheaterUnit,
)
from game.utils import Distance, meters, nautical_miles
from game.utils import Distance, feet, meters, nautical_miles
if TYPE_CHECKING:
from game.coalition import Coalition
@@ -204,6 +204,19 @@ class WaypointBuilder:
pretty_name="Refuel",
)
def recovery_tanker(self, position: Point) -> FlightWaypoint:
alt_type: AltitudeReference = "BARO"
return FlightWaypoint(
"RECOVERY",
FlightWaypointType.RECOVERY_TANKER,
position,
feet(6000),
alt_type,
description="Recovery tanker for aircraft carriers",
pretty_name="Recovery",
)
def split(self, position: Point) -> FlightWaypoint:
alt_type: AltitudeReference = "BARO"
if self.is_helo:

View File

@@ -21,6 +21,36 @@ class FlightState(ABC):
self.settings = settings
self.avoid_further_combat = False
def reinitialize(self, now: datetime) -> None:
from game.ato.flightstate import WaitingForStart
if self.flight.flight_plan.startup_time() <= now:
self._set_active_flight_state(now)
else:
self.flight.set_state(WaitingForStart(self.flight, self.settings))
def _set_active_flight_state(self, now: datetime) -> None:
from game.ato.flightstate import StartUp
from game.ato.flightstate import Taxi
from game.ato.flightstate import Takeoff
from game.ato.flightstate import Navigating
match self.flight.start_type:
case StartType.COLD:
self.flight.set_state(StartUp(self.flight, self.settings, now))
case StartType.WARM:
self.flight.set_state(Taxi(self.flight, self.settings, now))
case StartType.RUNWAY:
self.flight.set_state(Takeoff(self.flight, self.settings, now))
case StartType.IN_FLIGHT:
self.flight.set_state(
Navigating(self.flight, self.settings, waypoint_index=0)
)
case _:
raise ValueError(
f"Unknown start type {self.flight.start_type} for {self.flight}"
)
@property
def alive(self) -> bool:
return True

View File

@@ -20,11 +20,12 @@ class Uninitialized(FlightState):
def on_game_tick(
self, events: GameUpdateEvents, time: datetime, duration: timedelta
) -> None:
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
self.reinitialize(time)
self.flight.state.on_game_tick(events, time, duration)
@property
def is_waiting_for_start(self) -> bool:
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
return True
def estimate_position(self) -> Point:
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
@@ -35,7 +36,6 @@ class Uninitialized(FlightState):
@property
def description(self) -> str:
delay = self.flight.flight_plan.startup_time()
if self.flight.start_type is StartType.COLD:
action = "Starting up"
elif self.flight.start_type is StartType.WARM:
@@ -46,4 +46,4 @@ class Uninitialized(FlightState):
action = "In flight"
else:
raise ValueError(f"Unhandled StartType: {self.flight.start_type}")
return f"{action} in {delay}"
return f"{action} at {self.flight.flight_plan.startup_time():%H:%M:%S}"

View File

@@ -18,19 +18,17 @@ if TYPE_CHECKING:
class WaitingForStart(AtDeparture):
def __init__(
self,
flight: Flight,
settings: Settings,
start_time: datetime,
) -> None:
def __init__(self, flight: Flight, settings: Settings) -> None:
super().__init__(flight, settings)
self.start_time = start_time
@property
def start_type(self) -> StartType:
return self.flight.start_type
@property
def start_time(self) -> datetime:
return self.flight.flight_plan.startup_time()
def on_game_tick(
self, events: GameUpdateEvents, time: datetime, duration: timedelta
) -> None:

View File

@@ -24,8 +24,8 @@ class FlightType(Enum):
* Implementations of MissionTarget.mission_types: A mission type can only be planned
against compatible targets. The mission_types method of each target class defines
which missions may target it.
* ai_flight_planner_db.py: Add the new mission type to aircraft_for_task that
returns the list of compatible aircraft in order of preference.
* resources/units/aircraft/*.yaml: Assign aircraft weight for the new task type in
the `tasks` dict for all capable aircraft.
You may also need to update:

View File

@@ -1,8 +1,7 @@
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass, field
from datetime import timedelta
from datetime import datetime
from typing import Literal, TYPE_CHECKING
from dcs import Point
@@ -12,8 +11,7 @@ from game.theater.theatergroup import TheaterUnit
from game.utils import Distance, meters
if TYPE_CHECKING:
from game.theater import ControlPoint, MissionTarget
from game.theater import ControlPoint
AltitudeReference = Literal["BARO", "RADIO"]
@@ -32,7 +30,7 @@ class FlightWaypoint:
# having three names. A short and long form is enough.
description: str = ""
targets: Sequence[MissionTarget | TheaterUnit] = field(default_factory=list)
targets: list[TheaterUnit] = field(default_factory=list)
obj_name: str = ""
pretty_name: str = ""
only_for_player: bool = False
@@ -45,8 +43,8 @@ class FlightWaypoint:
# 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.
tot: timedelta | None = None
departure_time: timedelta | None = None
tot: datetime | None = None
departure_time: datetime | None = None
@property
def x(self) -> float:

View File

@@ -49,3 +49,4 @@ class FlightWaypointType(IntEnum):
REFUEL = 29 # Should look for nearby tanker to refuel from.
CARGO_STOP = 30 # Stopover landing point using the LandingReFuAr waypoint type
INGRESS_AIR_ASSAULT = 31
RECOVERY_TANKER = 32

View File

@@ -92,6 +92,9 @@ class Loadout:
if self.has_weapon_of_type(WeaponType.TGP):
return
if unit_type.has_built_in_target_pod:
return
new_pylons = dict(self.pylons)
for pylon_number, weapon in self.pylons.items():
if weapon is not None and weapon.weapon_group.type is WeaponType.LGB:

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import logging
from collections import defaultdict
from datetime import timedelta
from datetime import datetime
from typing import Dict, Optional, TYPE_CHECKING
from game.db import Database
@@ -33,8 +33,11 @@ class Package:
self.auto_asap = auto_asap
self.flights: list[Flight] = []
# Desired TOT as an offset from mission start.
self.time_over_target: timedelta = timedelta()
# Desired TOT as an offset from mission start. Obviously datetime.min is bogus,
# but it's going to be replaced by whatever is scheduling the package very soon.
# TODO: Constructor should maybe take the current time and use that to preserve
# the old behavior?
self.time_over_target: datetime = datetime.min
self.waypoints: PackageWaypoints | None = None
@property
@@ -62,7 +65,7 @@ class Package:
# TODO: Should depend on the type of escort.
# SEAD might be able to leave before CAP.
@property
def escort_start_time(self) -> Optional[timedelta]:
def escort_start_time(self) -> datetime | None:
times = []
for flight in self.flights:
waypoint = flight.flight_plan.request_escort_at()
@@ -81,7 +84,7 @@ class Package:
return None
@property
def escort_end_time(self) -> Optional[timedelta]:
def escort_end_time(self) -> datetime | None:
times = []
for flight in self.flights:
waypoint = flight.flight_plan.dismiss_escort_at()
@@ -103,7 +106,7 @@ class Package:
return None
@property
def mission_departure_time(self) -> Optional[timedelta]:
def mission_departure_time(self) -> datetime | None:
times = []
for flight in self.flights:
times.append(flight.flight_plan.mission_departure_time)
@@ -111,8 +114,19 @@ class Package:
return max(times)
return None
def set_tot_asap(self) -> None:
self.time_over_target = TotEstimator(self).earliest_tot()
def set_tot_asap(self, now: datetime) -> None:
self.time_over_target = TotEstimator(self).earliest_tot(now)
def clamp_tot_for_current_time(self, now: datetime) -> None:
if not self.all_flights_waiting_for_start():
return
if not self.flights:
return
earliest_startup_time = min(f.flight_plan.startup_time() for f in self.flights)
if earliest_startup_time < now:
self.time_over_target += now - earliest_startup_time
def add_flight(self, flight: Flight) -> None:
"""Adds a flight to the package."""
@@ -204,3 +218,14 @@ class Package:
if flight.departure == airfield:
return airfield
raise RuntimeError("Could not find any airfield assigned to this package")
def all_flights_waiting_for_start(self) -> bool:
"""Returns True if all flights in the package are waiting for start."""
for flight in self.flights:
if not flight.state.is_waiting_for_start:
return False
return True
def has_flight_with_task(self, task: FlightType) -> bool:
"""Returns True if any flight in the package has the given task."""
return task in (f.flight_type for f in self.flights)

View File

@@ -1,7 +1,6 @@
from __future__ import annotations
import math
from datetime import timedelta
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from dcs.mapping import Point
@@ -56,44 +55,24 @@ class TotEstimator:
def __init__(self, package: Package) -> None:
self.package = package
def earliest_tot(self) -> timedelta:
def earliest_tot(self, now: datetime) -> datetime:
if not self.package.flights:
return timedelta(0)
return now
earliest_tot = max(
(self.earliest_tot_for_flight(f) for f in self.package.flights)
)
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
# and they're not interesting from a mission planning perspective so we
# don't want them in the UI.
#
# Round up so we don't get negative start times.
return timedelta(seconds=math.ceil(earliest_tot.total_seconds()))
return max(self.earliest_tot_for_flight(f, now) for f in self.package.flights)
@staticmethod
def earliest_tot_for_flight(flight: Flight) -> timedelta:
"""Estimate the fastest time from mission start to the target position.
def earliest_tot_for_flight(flight: Flight, now: datetime) -> datetime:
"""Estimate the earliest time the flight can reach the target position.
For BARCAP flights, this is time to the racetrack 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.
The interpretation of the TOT depends on the flight plan type. See the various
FlightPlan implementations for details.
Args:
flight: The flight to get the earliest TOT time for.
flight: The flight to get the earliest TOT for.
now: The current mission time.
Returns:
The earliest possible TOT for the given flight in seconds. Returns 0
if an ingress point cannot be found.
The earliest possible TOT for the given flight.
"""
# Clear the TOT, calculate the startup time. Negating the result gives
# the earliest possible start time.
orig_tot = flight.package.time_over_target
try:
flight.package.time_over_target = timedelta()
time = flight.flight_plan.startup_time()
finally:
flight.package.time_over_target = orig_tot
return -time
return now + flight.flight_plan.minimum_duration_from_start_to_tot()

View File

@@ -5,22 +5,24 @@ import logging
from collections.abc import Iterator
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, Tuple
from typing import Any, Dict, TYPE_CHECKING, Tuple
import yaml
from packaging.version import Version
from game import persistency
from game import persistence
from game.profiling import logged_duration
from game.theater import (
ConflictTheater,
)
from game.theater import ConflictTheater
from game.theater.iadsnetwork.iadsnetwork import IadsNetwork
from game.theater.theaterloader import TheaterLoader
from game.version import CAMPAIGN_FORMAT_VERSION
from .campaignairwingconfig import CampaignAirWingConfig
from .factionrecommendation import FactionRecommendation
from .mizcampaignloader import MizCampaignLoader
if TYPE_CHECKING:
from game.factions.factions import Factions
PERF_FRIENDLY = 0
PERF_MEDIUM = 1
PERF_HARD = 2
@@ -40,8 +42,8 @@ class Campaign:
#: selecting a campaign that is not up to date.
version: Tuple[int, int]
recommended_player_faction: str
recommended_enemy_faction: str
recommended_player_faction: FactionRecommendation
recommended_enemy_faction: FactionRecommendation
recommended_start_date: datetime.date | None
recommended_start_time: datetime.time | None
@@ -57,10 +59,9 @@ class Campaign:
@classmethod
def from_file(cls, path: Path) -> Campaign:
with path.open() as campaign_file:
with path.open(encoding="utf-8") as campaign_file:
data = yaml.safe_load(campaign_file)
sanitized_theater = data["theater"].replace(" ", "")
version_field = data.get("version", "0")
try:
version = Version(version_field)
@@ -93,8 +94,12 @@ class Campaign:
data.get("authors", "???"),
data.get("description", ""),
(version.major, version.minor),
data.get("recommended_player_faction", "USA 2005"),
data.get("recommended_enemy_faction", "Russia 1990"),
FactionRecommendation.from_field(
data.get("recommended_player_faction"), player=True
),
FactionRecommendation.from_field(
data.get("recommended_enemy_faction"), player=False
),
start_date,
start_time,
data.get("recommended_player_money", DEFAULT_BUDGET),
@@ -163,6 +168,10 @@ class Campaign:
return False
return True
def register_campaign_specific_factions(self, factions: Factions) -> None:
self.recommended_player_faction.register_campaign_specific_faction(factions)
self.recommended_enemy_faction.register_campaign_specific_faction(factions)
@staticmethod
def iter_campaigns_in_dir(path: Path) -> Iterator[Path]:
yield from path.glob("*.yaml")
@@ -171,7 +180,7 @@ class Campaign:
@classmethod
def iter_campaign_defs(cls) -> Iterator[Path]:
yield from cls.iter_campaigns_in_dir(
Path(persistency.base_path()) / "Liberation/Campaigns"
Path(persistence.base_path()) / "Liberation/Campaigns"
)
yield from cls.iter_campaigns_in_dir(Path("resources/campaigns"))

View File

@@ -11,11 +11,15 @@ if TYPE_CHECKING:
from game.theater import ConflictTheater
DEFAULT_SQUADRON_SIZE = 12
@dataclass(frozen=True)
class SquadronConfig:
primary: FlightType
secondary: list[FlightType]
aircraft: list[str]
max_size: int
name: Optional[str]
nickname: Optional[str]
@@ -39,6 +43,7 @@ class SquadronConfig:
FlightType(data["primary"]),
secondary,
data.get("aircraft", []),
data.get("size", DEFAULT_SQUADRON_SIZE),
data.get("name", None),
data.get("nickname", None),
data.get("female_pilot_percentage", None),

View File

@@ -1,13 +1,12 @@
from __future__ import annotations
import dataclasses
import logging
from typing import Optional, TYPE_CHECKING, Dict, Union
from typing import Optional, TYPE_CHECKING
from game.squadrons import Squadron
from game.squadrons.squadrondef import SquadronDef
from ..ato.flighttype import FlightType
from .campaignairwingconfig import CampaignAirWingConfig, SquadronConfig
from ..ato.flighttype import FlightType
from ..dcs.aircrafttype import AircraftType
from ..theater import ControlPoint
@@ -44,7 +43,12 @@ class DefaultSquadronAssigner:
continue
squadron = Squadron.create_from(
squadron_def, control_point, self.coalition, self.game
squadron_def,
squadron_config.primary,
squadron_config.max_size,
control_point,
self.coalition,
self.game,
)
squadron.set_auto_assignable_mission_types(
squadron_config.auto_assignable
@@ -54,7 +58,6 @@ class DefaultSquadronAssigner:
def find_squadron_for(
self, config: SquadronConfig, control_point: ControlPoint
) -> Optional[SquadronDef]:
for preferred_aircraft in config.aircraft:
squadron_def = self.find_preferred_squadron(
preferred_aircraft, config.primary, control_point
@@ -62,13 +65,14 @@ class DefaultSquadronAssigner:
if squadron_def is not None:
return squadron_def
# If we didn't find any of the preferred types we should use any squadron
# If we didn't find any of the preferred types (if the list contains only
# squadrons or aircraft unavailable to the coalition) we should use any squadron
# compatible with the primary task.
squadron_def = self.find_squadron_for_task(config.primary, control_point)
if squadron_def is not None:
return squadron_def
# If we can't find any squadron matching the requirement, we should
# If we can't find any pre-made squadron matching the requirement, we should
# create one.
return self.air_wing.squadron_def_generator.generate_for_task(
config.primary, control_point
@@ -89,7 +93,11 @@ class DefaultSquadronAssigner:
try:
aircraft = AircraftType.named(preferred_aircraft)
except KeyError:
# No aircraft with this name.
logging.warning(
"%s is neither a compatible squadron or a known aircraft type, "
"ignoring",
preferred_aircraft,
)
return None
if aircraft not in self.coalition.faction.aircrafts:
@@ -112,7 +120,7 @@ class DefaultSquadronAssigner:
) -> bool:
if ignore_base_preference:
return control_point.can_operate(squadron.aircraft)
return squadron.operates_from(control_point) and task in squadron.mission_types
return squadron.operates_from(control_point) and squadron.capable_of(task)
def find_squadron_for_airframe(
self, aircraft: AircraftType, task: FlightType, control_point: ControlPoint

View File

@@ -0,0 +1,53 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, TYPE_CHECKING
from game.factions import Faction
if TYPE_CHECKING:
from game.factions.factions import Factions
class FactionRecommendation(ABC):
def __init__(self, name: str) -> None:
self.name = name
@abstractmethod
def register_campaign_specific_faction(self, factions: Factions) -> None:
...
@abstractmethod
def get_faction(self, factions: Factions) -> Faction:
...
@staticmethod
def from_field(
data: str | dict[str, Any] | None, player: bool
) -> FactionRecommendation:
if data is None:
name = "USA 2005" if player else "Russia 1990"
return BuiltinFactionRecommendation(name)
if isinstance(data, str):
return BuiltinFactionRecommendation(data)
return CampaignDefinedFactionRecommendation(Faction.from_dict(data))
class BuiltinFactionRecommendation(FactionRecommendation):
def register_campaign_specific_faction(self, factions: Factions) -> None:
pass
def get_faction(self, factions: Factions) -> Faction:
return factions.get_by_name(self.name)
class CampaignDefinedFactionRecommendation(FactionRecommendation):
def __init__(self, faction: Faction) -> None:
super().__init__(faction.name)
self.faction = faction
def register_campaign_specific_faction(self, factions: Factions) -> None:
factions.add_campaign_defined(self.faction)
def get_faction(self, factions: Factions) -> Faction:
return self.faction

View File

@@ -9,7 +9,6 @@ from game.dcs.aircrafttype import AircraftType
from game.squadrons.operatingbases import OperatingBases
from game.squadrons.squadrondef import SquadronDef
from game.theater import ControlPoint
from game.ato.ai_flight_planner_db import aircraft_for_task, tasks_for_aircraft
if TYPE_CHECKING:
from game.factions.faction import Faction
@@ -25,7 +24,7 @@ class SquadronDefGenerator:
self, task: FlightType, control_point: ControlPoint
) -> Optional[SquadronDef]:
aircraft_choice: Optional[AircraftType] = None
for aircraft in aircraft_for_task(task):
for aircraft in AircraftType.priority_list_for_task(task):
if aircraft not in self.faction.aircrafts:
continue
if not control_point.can_operate(aircraft):
@@ -48,7 +47,7 @@ class SquadronDefGenerator:
role="Flying Squadron",
aircraft=aircraft,
livery=None,
mission_types=tuple(tasks_for_aircraft(aircraft)),
auto_assignable_mission_types=set(aircraft.iter_task_capabilities()),
operating_bases=OperatingBases.default_for_aircraft(aircraft),
female_pilot_percentage=6,
pilot_pool=[],

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
from datetime import datetime
from typing import Any, Optional, TYPE_CHECKING
from faker import Faker
@@ -157,14 +158,14 @@ class Coalition:
# is handled correctly.
self.transfers.perform_transfers()
def preinit_turn_0(self) -> None:
def preinit_turn_0(self, squadrons_start_full: bool) -> None:
"""Runs final Coalition initialization.
Final initialization occurs before Game.initialize_turn runs for turn 0.
"""
self.air_wing.populate_for_turn_0()
self.air_wing.populate_for_turn_0(squadrons_start_full)
def initialize_turn(self) -> None:
def initialize_turn(self, is_turn_0: bool) -> None:
"""Processes coalition-specific turn initialization.
For more information on turn initialization in general, see the documentation
@@ -181,9 +182,10 @@ class Coalition:
with logged_duration("Procurement of airlift assets"):
self.transfers.order_airlift_assets()
with logged_duration("Transport planning"):
self.transfers.plan_transports()
self.transfers.plan_transports(self.game.conditions.start_time)
self.plan_missions()
if not is_turn_0 or not self.game.settings.enable_squadron_aircraft_limits:
self.plan_missions(self.game.conditions.start_time)
self.plan_procurement()
def refund_outstanding_orders(self) -> None:
@@ -199,16 +201,16 @@ class Coalition:
for squadron in self.air_wing.iter_squadrons():
squadron.refund_orders()
def plan_missions(self) -> None:
def plan_missions(self, now: datetime) -> None:
color = "Blue" if self.player else "Red"
with MultiEventTracer() as tracer:
with tracer.trace(f"{color} mission planning"):
with tracer.trace(f"{color} mission identification"):
TheaterCommander(self.game, self.player).plan_missions(tracer)
TheaterCommander(self.game, self.player).plan_missions(now, tracer)
with tracer.trace(f"{color} mission scheduling"):
MissionScheduler(
self, self.game.settings.desired_player_mission_duration
).schedule_missions()
).schedule_missions(now)
def plan_procurement(self) -> None:
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it gets much

View File

@@ -3,12 +3,12 @@ from __future__ import annotations
import logging
import random
from collections import defaultdict
from datetime import timedelta
from typing import Iterator, Dict, TYPE_CHECKING
from datetime import datetime, timedelta
from typing import Iterator, TYPE_CHECKING
from game.theater import MissionTarget
from game.ato.flighttype import FlightType
from game.ato.traveltime import TotEstimator
from game.theater import MissionTarget
if TYPE_CHECKING:
from game.coalition import Coalition
@@ -19,7 +19,7 @@ class MissionScheduler:
self.coalition = coalition
self.desired_mission_length = desired_mission_length
def schedule_missions(self) -> None:
def schedule_missions(self, now: datetime) -> None:
"""Identifies and plans mission for the turn."""
def start_time_generator(
@@ -35,7 +35,7 @@ class MissionScheduler:
FlightType.TARCAP,
}
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(timedelta)
previous_cap_end_time: dict[MissionTarget, datetime] = defaultdict(now.replace)
non_dca_packages = [
p for p in self.coalition.ato.packages if p.primary_task not in dca_types
]
@@ -47,7 +47,7 @@ class MissionScheduler:
margin=5 * 60,
)
for package in self.coalition.ato.packages:
tot = TotEstimator(package).earliest_tot()
tot = TotEstimator(package).earliest_tot(now)
if package.primary_task in dca_types:
previous_end_time = previous_cap_end_time[package.target]
if tot > previous_end_time:
@@ -65,7 +65,7 @@ class MissionScheduler:
continue
previous_cap_end_time[package.target] = departure_time
elif package.auto_asap:
package.set_tot_asap()
package.set_tot_asap(now)
else:
# But other packages should be spread out a bit. Note that take
# times are delayed, but all aircraft will become active at

View File

@@ -5,6 +5,7 @@ import operator
from collections.abc import Iterable, Iterator
from typing import TYPE_CHECKING, TypeVar
from game.ato.closestairfields import ClosestAirfields, ObjectiveDistanceCache
from game.theater import (
Airfield,
ControlPoint,
@@ -15,12 +16,11 @@ from game.theater import (
)
from game.theater.theatergroundobject import (
BuildingGroundObject,
IadsBuildingGroundObject,
IadsGroundObject,
NavalGroundObject,
IadsBuildingGroundObject,
)
from game.utils import meters, nautical_miles
from game.ato.closestairfields import ClosestAirfields, ObjectiveDistanceCache
if TYPE_CHECKING:
from game import Game
@@ -117,7 +117,7 @@ class ObjectiveFinder:
if isinstance(
ground_object, IadsBuildingGroundObject
) and not self.game.settings.plugin_option("skynetiads"):
) and not self.game.lua_plugin_manager.is_plugin_enabled("skynetiads"):
# Prevent strike targets on IADS Buildings when skynet features
# are disabled as they do not serve any purpose
continue
@@ -209,22 +209,20 @@ class ObjectiveFinder:
raise RuntimeError("Found no friendly control points. You probably lost.")
return farthest
def closest_friendly_control_point(self) -> ControlPoint:
def preferred_theater_refueling_control_point(self) -> ControlPoint | None:
"""Finds the friendly control point that is closest to any threats."""
threat_zones = self.game.threat_zone_for(not self.is_player)
closest = None
min_distance = meters(math.inf)
for cp in self.friendly_control_points():
if isinstance(cp, OffMapSpawn):
if isinstance(cp, OffMapSpawn) or cp.is_fleet:
continue
distance = threat_zones.distance_to_threat(cp.position)
if distance < min_distance:
closest = cp
min_distance = distance
if closest is None:
raise RuntimeError("Found no friendly control points. You probably lost.")
return closest
def enemy_control_points(self) -> Iterator[ControlPoint]:

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import logging
from collections import defaultdict
from datetime import datetime
from typing import Dict, Iterable, Optional, Set, TYPE_CHECKING
from game.ato.airtaaskingorder import AirTaskingOrder
@@ -132,6 +133,7 @@ class PackageFulfiller:
self,
mission: ProposedMission,
purchase_multiplier: int,
now: datetime,
tracer: MultiEventTracer,
) -> Optional[Package]:
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
@@ -221,6 +223,6 @@ class PackageFulfiller:
if package.has_players and self.player_missions_asap:
package.auto_asap = True
package.set_tot_asap()
package.set_tot_asap(now)
return package

View File

@@ -31,8 +31,7 @@ class RangeType(IntEnum):
# TODO: Refactor so that we don't need to call up to the mission planner.
# Bypass type checker due to https://github.com/python/mypy/issues/5374
@dataclass # type: ignore
@dataclass
class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
target: MissionTargetT
flights: list[ProposedFlight] = field(init=False)
@@ -104,6 +103,7 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
self.package = fulfiller.plan_mission(
ProposedMission(self.target, self.flights),
self.purchase_multiplier,
state.context.now,
state.context.tracer,
)
return self.package is not None

View File

@@ -54,6 +54,7 @@ https://en.wikipedia.org/wiki/Hierarchical_task_network
"""
from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING
from game.ato.starttype import StartType
@@ -77,8 +78,8 @@ class TheaterCommander(Planner[TheaterState, TheaterCommanderTask]):
self.game = game
self.player = player
def plan_missions(self, tracer: MultiEventTracer) -> None:
state = TheaterState.from_game(self.game, self.player, tracer)
def plan_missions(self, now: datetime, tracer: MultiEventTracer) -> None:
state = TheaterState.from_game(self.game, self.player, now, tracer)
while True:
result = self.plan(state)
if result is None:

View File

@@ -5,6 +5,7 @@ import itertools
import math
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import datetime
from typing import Optional, TYPE_CHECKING, Union
from game.commander.battlepositions import BattlePositions
@@ -36,6 +37,7 @@ class PersistentContext:
coalition: Coalition
theater: ConflictTheater
turn: int
now: datetime
settings: Settings
tracer: MultiEventTracer
@@ -137,14 +139,20 @@ class TheaterState(WorldState["TheaterState"]):
@classmethod
def from_game(
cls, game: Game, player: bool, tracer: MultiEventTracer
cls, game: Game, player: bool, now: datetime, tracer: MultiEventTracer
) -> TheaterState:
coalition = game.coalition_for(player)
finder = ObjectiveFinder(game, player)
ordered_capturable_points = finder.prioritized_unisolated_points()
context = PersistentContext(
game.db, coalition, game.theater, game.turn, game.settings, tracer
game.db,
coalition,
game.theater,
game.turn,
now,
game.settings,
tracer,
)
# Plan enough rounds of CAP that the target has coverage over the expected
@@ -153,6 +161,11 @@ class TheaterState(WorldState["TheaterState"]):
barcap_duration = coalition.doctrine.cap_duration.total_seconds()
barcap_rounds = math.ceil(mission_duration / barcap_duration)
refueling_targets: list[MissionTarget] = []
theater_refuling_point = finder.preferred_theater_refueling_control_point()
if theater_refuling_point is not None:
refueling_targets.append(theater_refuling_point)
return TheaterState(
context=context,
barcaps_needed={
@@ -162,7 +175,7 @@ class TheaterState(WorldState["TheaterState"]):
front_line_stances={f: None for f in finder.front_lines()},
vulnerable_front_lines=list(finder.front_lines()),
aewc_targets=[finder.farthest_friendly_control_point()],
refueling_targets=[finder.closest_friendly_control_point()],
refueling_targets=refueling_targets,
enemy_air_defenses=list(finder.enemy_air_defenses()),
threatening_air_defenses=[],
detecting_air_defenses=[],

View File

@@ -97,6 +97,7 @@ class WeaponType(Enum):
ARM = "ARM"
LGB = "LGB"
TGP = "TGP"
DECOY = "decoy"
UNKNOWN = "unknown"

View File

@@ -1,10 +1,11 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from functools import cached_property
from functools import cache, cached_property
from pathlib import Path
from typing import Any, Dict, Iterator, Optional, TYPE_CHECKING, Type
from typing import Any, ClassVar, Dict, Iterator, Optional, TYPE_CHECKING, Type
import yaml
from dcs.helicopters import helicopter_map
@@ -20,6 +21,7 @@ from game.radio.channels import (
CommonRadioChannelAllocator,
FarmerRadioChannelAllocator,
HueyChannelNamer,
LegacyWarthogChannelNamer,
MirageChannelNamer,
MirageF1CEChannelNamer,
NoOpChannelAllocator,
@@ -31,6 +33,7 @@ from game.radio.channels import (
ViggenChannelNamer,
ViggenRadioChannelAllocator,
ViperChannelNamer,
WarthogChannelNamer,
)
from game.utils import (
Distance,
@@ -47,6 +50,7 @@ from game.utils import (
)
if TYPE_CHECKING:
from game.ato import FlightType
from game.missiongenerator.aircraft.flightdata import FlightData
from game.missiongenerator.missiondata import MissionData
from game.radio.radios import Radio, RadioFrequency, RadioRegistry
@@ -104,6 +108,8 @@ class RadioConfig:
"viggen": ViggenChannelNamer,
"viper": ViperChannelNamer,
"apache": ApacheChannelNamer,
"a10c-legacy": LegacyWarthogChannelNamer,
"a10c-ii": WarthogChannelNamer,
}[config.get("namer", "default")]
@@ -192,6 +198,23 @@ class AircraftType(UnitType[Type[FlyingType]]):
# will be set to true for helos by default
can_carry_crates: bool
task_priorities: dict[FlightType, int]
# Set to True when aircraft mounts a targeting pod by default i.e. the pod does
# not take up a weapons station. If True, do not replace LGBs with dumb bombs
# when no TGP is mounted on any station.
has_built_in_target_pod: bool
_by_name: ClassVar[dict[str, AircraftType]] = {}
_by_unit_type: ClassVar[dict[type[FlyingType], list[AircraftType]]] = defaultdict(
list
)
@classmethod
def register(cls, unit_type: AircraftType) -> None:
cls._by_name[unit_type.name] = unit_type
cls._by_unit_type[unit_type.dcs_unit_type].append(unit_type)
@property
def flyable(self) -> bool:
return self.dcs_unit_type.flyable
@@ -302,6 +325,12 @@ class AircraftType(UnitType[Type[FlyingType]]):
def iter_props(self) -> Iterator[UnitProperty[Any]]:
return UnitProperty.for_aircraft(self.dcs_unit_type)
def capable_of(self, task: FlightType) -> bool:
return task in self.task_priorities
def task_priority(self, task: FlightType) -> int:
return self.task_priorities[task]
def __setstate__(self, state: dict[str, Any]) -> None:
# Update any existing models with new data on load.
updated = AircraftType.named(state["name"])
@@ -312,17 +341,31 @@ class AircraftType(UnitType[Type[FlyingType]]):
def named(cls, name: str) -> AircraftType:
if not cls._loaded:
cls._load_all()
unit = cls._by_name[name]
assert isinstance(unit, AircraftType)
return unit
return cls._by_name[name]
@classmethod
def for_dcs_type(cls, dcs_unit_type: Type[FlyingType]) -> Iterator[AircraftType]:
if not cls._loaded:
cls._load_all()
for unit in cls._by_unit_type[dcs_unit_type]:
assert isinstance(unit, AircraftType)
yield unit
yield from cls._by_unit_type[dcs_unit_type]
@classmethod
def iter_all(cls) -> Iterator[AircraftType]:
if not cls._loaded:
cls._load_all()
yield from cls._by_name.values()
@classmethod
@cache
def priority_list_for_task(cls, task: FlightType) -> list[AircraftType]:
capable = []
for aircraft in cls.iter_all():
if aircraft.capable_of(task):
capable.append(aircraft)
return list(reversed(sorted(capable, key=lambda a: a.task_priority(task))))
def iter_task_capabilities(self) -> Iterator[FlightType]:
yield from self.task_priorities
@staticmethod
def each_dcs_type() -> Iterator[Type[FlyingType]]:
@@ -348,6 +391,8 @@ class AircraftType(UnitType[Type[FlyingType]]):
@classmethod
def _each_variant_of(cls, aircraft: Type[FlyingType]) -> Iterator[AircraftType]:
from game.ato.flighttype import FlightType
data_path = Path("resources/units/aircraft") / f"{aircraft.id}.yaml"
if not data_path.exists():
logging.warning(f"No data for {aircraft.id}; it will not be available")
@@ -404,6 +449,10 @@ class AircraftType(UnitType[Type[FlyingType]]):
if prop_overrides is not None:
cls._set_props_overrides(prop_overrides, aircraft, data_path)
task_priorities: dict[FlightType, int] = {}
for task_name, priority in data.get("tasks", {}).items():
task_priorities[FlightType(task_name)] = priority
for variant in data.get("variants", [aircraft.id]):
yield AircraftType(
dcs_unit_type=aircraft,
@@ -435,4 +484,9 @@ class AircraftType(UnitType[Type[FlyingType]]):
unit_class=unit_class,
cabin_size=data.get("cabin_size", 10 if aircraft.helicopter else 0),
can_carry_crates=data.get("can_carry_crates", aircraft.helicopter),
task_priorities=task_priorities,
has_built_in_target_pod=data.get("has_built_in_target_pod", False),
)
def __hash__(self) -> int:
return hash(self.name)

View File

@@ -1,9 +1,10 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterator, Optional, Type
from typing import Any, ClassVar, Iterator, Optional, Type
import yaml
from dcs.unittype import VehicleType
@@ -59,21 +60,27 @@ class GroundUnitType(UnitType[Type[VehicleType]]):
# Some units like few Launchers have to be placed backwards to be able to fire.
reversed_heading: bool = False
_by_name: ClassVar[dict[str, GroundUnitType]] = {}
_by_unit_type: ClassVar[
dict[type[VehicleType], list[GroundUnitType]]
] = defaultdict(list)
@classmethod
def register(cls, unit_type: GroundUnitType) -> None:
cls._by_name[unit_type.name] = unit_type
cls._by_unit_type[unit_type.dcs_unit_type].append(unit_type)
@classmethod
def named(cls, name: str) -> GroundUnitType:
if not cls._loaded:
cls._load_all()
unit = cls._by_name[name]
assert isinstance(unit, GroundUnitType)
return unit
return cls._by_name[name]
@classmethod
def for_dcs_type(cls, dcs_unit_type: Type[VehicleType]) -> Iterator[GroundUnitType]:
if not cls._loaded:
cls._load_all()
for unit in cls._by_unit_type[dcs_unit_type]:
assert isinstance(unit, GroundUnitType)
yield unit
yield from cls._by_unit_type[dcs_unit_type]
@staticmethod
def each_dcs_type() -> Iterator[Type[VehicleType]]:

View File

@@ -1,9 +1,10 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Iterator, Type
from typing import ClassVar, Iterator, Type
import yaml
from dcs.ships import ship_map
@@ -15,21 +16,27 @@ from game.dcs.unittype import UnitType
@dataclass(frozen=True)
class ShipUnitType(UnitType[Type[ShipType]]):
_by_name: ClassVar[dict[str, ShipUnitType]] = {}
_by_unit_type: ClassVar[dict[type[ShipType], list[ShipUnitType]]] = defaultdict(
list
)
@classmethod
def register(cls, unit_type: ShipUnitType) -> None:
cls._by_name[unit_type.name] = unit_type
cls._by_unit_type[unit_type.dcs_unit_type].append(unit_type)
@classmethod
def named(cls, name: str) -> ShipUnitType:
if not cls._loaded:
cls._load_all()
unit = cls._by_name[name]
assert isinstance(unit, ShipUnitType)
return unit
return cls._by_name[name]
@classmethod
def for_dcs_type(cls, dcs_unit_type: Type[ShipType]) -> Iterator[ShipUnitType]:
if not cls._loaded:
cls._load_all()
for unit in cls._by_unit_type[dcs_unit_type]:
assert isinstance(unit, ShipUnitType)
yield unit
yield from cls._by_unit_type[dcs_unit_type]
@staticmethod
def each_dcs_type() -> Iterator[Type[ShipType]]:

View File

@@ -1,10 +1,9 @@
from __future__ import annotations
from abc import ABC
from collections import defaultdict
from dataclasses import dataclass
from functools import cached_property
from typing import Any, ClassVar, Generic, Iterator, Type, TypeVar
from typing import ClassVar, Generic, Iterator, Self, Type, TypeVar
from dcs.unittype import UnitType as DcsUnitType
@@ -25,10 +24,6 @@ class UnitType(ABC, Generic[DcsUnitTypeT]):
price: int
unit_class: UnitClass
_by_name: ClassVar[dict[str, UnitType[Any]]] = {}
_by_unit_type: ClassVar[dict[Type[DcsUnitType], list[UnitType[Any]]]] = defaultdict(
list
)
_loaded: ClassVar[bool] = False
def __str__(self) -> str:
@@ -39,16 +34,15 @@ class UnitType(ABC, Generic[DcsUnitTypeT]):
return self.dcs_unit_type.id
@classmethod
def register(cls, unit_type: UnitType[Any]) -> None:
cls._by_name[unit_type.name] = unit_type
cls._by_unit_type[unit_type.dcs_unit_type].append(unit_type)
@classmethod
def named(cls, name: str) -> UnitType[Any]:
def register(cls, unit_type: Self) -> None:
raise NotImplementedError
@classmethod
def for_dcs_type(cls, dcs_unit_type: DcsUnitTypeT) -> Iterator[UnitType[Any]]:
def named(cls, name: str) -> Self:
raise NotImplementedError
@classmethod
def for_dcs_type(cls, dcs_unit_type: DcsUnitTypeT) -> Iterator[Self]:
raise NotImplementedError
@staticmethod
@@ -56,7 +50,7 @@ class UnitType(ABC, Generic[DcsUnitTypeT]):
raise NotImplementedError
@classmethod
def _each_variant_of(cls, unit: DcsUnitTypeT) -> Iterator[UnitType[Any]]:
def _each_variant_of(cls, unit: DcsUnitTypeT) -> Iterator[Self]:
raise NotImplementedError
@classmethod

View File

@@ -109,16 +109,41 @@ class StateData:
base_capture_events: List[str]
@classmethod
def from_json(cls, data: Dict[str, Any]) -> StateData:
def from_json(cls, data: Dict[str, Any], unit_map: UnitMap) -> StateData:
def clean_unit_list(unit_list: List[Any]) -> List[str]:
# Cleans list of units in state.json by
# - Removing duplicates. Airfields emit a new "dead" event every time a bomb
# is dropped on them when they've already dead.
# - Normalise dead map objects (which are ints) to strings. The unit map
# only stores strings
units = set()
for unit in unit_list:
units.add(str(unit))
return list(units)
killed_aircraft = []
killed_ground_units = []
# Process killed units from S_EVENT_UNIT_LOST, S_EVENT_CRASH, S_EVENT_DEAD & S_EVENT_KILL
# Try to process every event that could indicate a unit was killed, even if it is
# inefficient and results in duplication as the logic DCS uses to trigger the various
# event types is not clear and may change over time.
killed_units = clean_unit_list(
data["unit_lost_events"]
+ data["kill_events"]
+ data["crash_events"]
+ data["dead_events"]
)
for unit in killed_units: # organize killed units into aircraft vs ground
if unit_map.flight(unit) is not None:
killed_aircraft.append(unit)
else:
killed_ground_units.append(unit)
return cls(
mission_ended=data["mission_ended"],
killed_aircraft=data["killed_aircrafts"],
# Airfields emit a new "dead" event every time a bomb is dropped on
# them when they've already dead. Dedup.
#
# Also normalize dead map objects (which are ints) to strings. The unit map
# only stores strings.
killed_ground_units=list({str(u) for u in data["killed_ground_units"]}),
killed_aircraft=killed_aircraft,
killed_ground_units=killed_ground_units,
destroyed_statics=data["destroyed_objects_positions"],
base_capture_events=data["base_capture_events"],
)
@@ -128,7 +153,7 @@ class Debriefing:
def __init__(
self, state_data: Dict[str, Any], game: Game, unit_map: UnitMap
) -> None:
self.state_data = StateData.from_json(state_data)
self.state_data = StateData.from_json(state_data, unit_map)
self.game = game
self.unit_map = unit_map
@@ -338,9 +363,7 @@ class Debriefing:
seen = set()
captures = []
for capture in reversed(self.state_data.base_capture_events):
# The ID string in the JSON file will be an airport ID for airport captures
# but will be a UUID for all other types, since DCS doesn't know the UUIDs
# for the captured FOBs.
# The ID string in the JSON file will be the UUID generated from liberation
cp_id, new_owner_id_str, _name = capture.split("||")
# Only the most recent capture event matters.
@@ -349,13 +372,8 @@ class Debriefing:
seen.add(cp_id)
try:
control_point = self.game.theater.find_control_point_by_airport_id(
int(cp_id)
)
except ValueError:
# The CP ID could not be converted to an int, so it's a UUID.
control_point = self.game.theater.find_control_point_by_id(UUID(cp_id))
except KeyError:
except (KeyError, ValueError):
# Captured base is not a part of the campaign. This happens when neutral
# bases are near the conflict. Nothing to do.
continue

View File

@@ -1,4 +1 @@
from .faction import Faction
from .factionloader import FactionLoader
FACTIONS = FactionLoader()

View File

@@ -4,33 +4,32 @@ import itertools
import logging
from dataclasses import dataclass, field
from functools import cached_property
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING
from typing import Any, Dict, Iterator, List, Optional, TYPE_CHECKING, Type
import dcs
from dcs.countries import country_dict
from dcs.unittype import ShipType, StaticType
from dcs.unittype import UnitType as DcsUnitType
from dcs.unittype import ShipType, StaticType, UnitType as DcsUnitType
from game.armedforces.forcegroup import ForceGroup
from game.data.building_data import (
WW2_ALLIES_BUILDINGS,
DEFAULT_AVAILABLE_BUILDINGS,
WW2_GERMANY_BUILDINGS,
WW2_FREE,
REQUIRED_BUILDINGS,
IADS_BUILDINGS,
REQUIRED_BUILDINGS,
WW2_ALLIES_BUILDINGS,
WW2_FREE,
WW2_GERMANY_BUILDINGS,
)
from game.data.doctrine import (
COLDWAR_DOCTRINE,
Doctrine,
MODERN_DOCTRINE,
COLDWAR_DOCTRINE,
WWII_DOCTRINE,
)
from game.data.units import UnitClass
from game.data.groups import GroupRole
from game.data.units import UnitClass
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.dcs.shipunittype import ShipUnitType
from game.armedforces.forcegroup import ForceGroup
from game.dcs.unittype import UnitType
if TYPE_CHECKING:
@@ -168,7 +167,7 @@ class Faction:
return sorted(air_defenses)
@classmethod
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
def from_dict(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
faction = Faction(locales=json.get("locales"))
faction.country = json.get("country", "/")
@@ -297,6 +296,9 @@ class Faction:
self.remove_aircraft("VSN_F104G")
self.remove_aircraft("VSN_F104S")
self.remove_aircraft("VSN_F104S_AG")
if not mod_settings.f4_phantom:
self.remove_aircraft("VSN_F4B")
self.remove_aircraft("VSN_F4C")
if not mod_settings.jas39_gripen:
self.remove_aircraft("JAS39Gripen")
self.remove_aircraft("JAS39Gripen_AG")

View File

@@ -1,54 +0,0 @@
from __future__ import annotations
import json
import logging
from pathlib import Path
from typing import Dict, Iterator, List, Optional, Type
from game import persistency
from game.factions.faction import Faction
FACTION_DIRECTORY = Path("./resources/factions/")
class FactionLoader:
def __init__(self) -> None:
self._factions: Optional[Dict[str, Faction]] = None
@property
def factions(self) -> Dict[str, Faction]:
self.initialize()
assert self._factions is not None
return self._factions
def initialize(self) -> None:
if self._factions is None:
self._factions = self.load_factions()
@staticmethod
def find_faction_files_in(path: Path) -> List[Path]:
return [f for f in path.glob("*.json") if f.is_file()]
@classmethod
def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]:
user_faction_path = Path(persistency.base_path()) / "Liberation/Factions"
files = cls.find_faction_files_in(
FACTION_DIRECTORY
) + cls.find_faction_files_in(user_faction_path)
factions = {}
for f in files:
try:
with f.open("r", encoding="utf-8") as fdata:
data = json.load(fdata)
factions[data["name"]] = Faction.from_json(data)
logging.info("Loaded faction : " + str(f))
except Exception:
logging.exception(f"Unable to load faction : {f}")
return factions
def __getitem__(self, name: str) -> Faction:
return self.factions[name]
def __iter__(self) -> Iterator[str]:
return iter(self.factions.keys())

70
game/factions/factions.py Normal file
View File

@@ -0,0 +1,70 @@
from __future__ import annotations
import itertools
import json
import logging
from collections.abc import Iterator
from pathlib import Path
import yaml
from game import persistence
from .faction import Faction
class Factions:
def __init__(self, factions: dict[str, Faction]) -> None:
self.factions = factions
self.campaign_defined_factions: dict[str, Faction] = {}
def get_by_name(self, name: str) -> Faction:
try:
return self.factions[name]
except KeyError:
return self.campaign_defined_factions[name]
def iter_faction_names(self) -> Iterator[str]:
# Campaign defined factions first so they show up at the top of the list in the
# UI.
return itertools.chain(self.campaign_defined_factions, self.factions)
def add_campaign_defined(self, faction: Faction) -> None:
if (
faction.name in self.factions
or faction.name in self.campaign_defined_factions
):
raise KeyError(f"Duplicate faction {faction.name}")
self.campaign_defined_factions[faction.name] = faction
def reset_campaign_defined(self) -> None:
self.campaign_defined_factions = {}
@staticmethod
def iter_faction_files_in(path: Path) -> Iterator[Path]:
yield from path.glob("*.json")
yield from path.glob("*.yaml")
@classmethod
def iter_faction_files(cls) -> Iterator[Path]:
yield from cls.iter_faction_files_in(Path("resources/factions/"))
yield from cls.iter_faction_files_in(
Path(persistence.base_path()) / "Liberation/Factions"
)
@classmethod
def load(cls) -> Factions:
factions = {}
for path in cls.iter_faction_files():
try:
with path.open("r", encoding="utf-8") as fdata:
if path.suffix == ".yaml":
data = yaml.safe_load(fdata)
else:
data = json.load(fdata)
faction = Faction.from_dict(data)
factions[faction.name] = faction
logging.info("Loaded faction from %s", path)
except Exception:
logging.exception(f"Unable to load faction from %s", path)
return Factions(factions)

View File

@@ -7,7 +7,6 @@ from collections.abc import Iterator
from datetime import date, datetime, time, timedelta
from enum import Enum
from typing import Any, List, TYPE_CHECKING, Type, Union, cast
from uuid import UUID
from dcs.countries import Switzerland, USAFAggressors, UnitedNationsPeacekeepers
from dcs.country import Country
@@ -17,16 +16,16 @@ from dcs.vehicles import AirDefence
from faker import Faker
from game.ato.closestairfields import ObjectiveDistanceCache
from game.ground_forces.ai_ground_planner import GroundPlanner
from game.models.game_stats import GameStats
from game.plugins import LuaPluginManager
from game.utils import Distance
from . import naming, persistency
from . import naming
from .ato.flighttype import FlightType
from .campaignloader import CampaignAirWingConfig
from .coalition import Coalition
from .db.gamedb import GameDb
from .infos.information import Information
from .persistence import SaveManager
from .profiling import logged_duration
from .settings import Settings
from .theater import ConflictTheater
@@ -38,7 +37,7 @@ from .theater.theatergroundobject import (
)
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
from .timeofday import TimeOfDay
from .weather import Conditions
from .weather.conditions import Conditions
if TYPE_CHECKING:
from .ato.airtaaskingorder import AirTaskingOrder
@@ -98,23 +97,24 @@ class Game:
start_date: datetime,
start_time: time | None,
settings: Settings,
lua_plugin_manager: LuaPluginManager,
player_budget: float,
enemy_budget: float,
) -> None:
self.settings = settings
self.lua_plugin_manager = lua_plugin_manager
self.theater = theater
self.turn = 0
# NB: This is the *start* date. It is never updated.
self.date = date(start_date.year, start_date.month, start_date.day)
self.game_stats = GameStats()
self.notes = ""
self.ground_planners: dict[UUID, GroundPlanner] = {}
self.informations: list[Information] = []
self.message("Game Start", "-" * 40)
# Culling Zones are for areas around points of interest that contain things we may not wish to cull.
self.__culling_zones: List[Point] = []
self.__destroyed_units: list[dict[str, Union[float, str]]] = []
self.savepath = ""
self.save_manager = SaveManager(self)
self.current_unit_id = 0
self.current_group_id = 0
self.name_generator = naming.namegen
@@ -229,7 +229,13 @@ class Game:
# We need to persist this state so that names generated after game load don't
# conflict with those generated before exit.
naming.namegen = self.name_generator
LuaPluginManager.load_settings(self.settings)
# The installed plugins may have changed between runs. We need to load the
# current configuration and patch in the options that were previously set.
new_plugin_manager = LuaPluginManager.load()
new_plugin_manager.update_with(self.lua_plugin_manager)
self.lua_plugin_manager = new_plugin_manager
ObjectiveDistanceCache.set_theater(self.theater)
self.compute_unculled_zones(GameUpdateEvents())
if not game_still_initializing:
@@ -290,7 +296,7 @@ class Game:
if self.turn > 1:
self.conditions = self.generate_conditions()
def begin_turn_0(self) -> None:
def begin_turn_0(self, squadrons_start_full: bool) -> None:
"""Initialization for the first turn of the game."""
from .sim import GameUpdateEvents
@@ -315,12 +321,16 @@ class Game:
# Rotate the whole TGO with the new heading
tgo.rotate(heading or tgo.heading)
self.blue.preinit_turn_0()
self.red.preinit_turn_0()
self.blue.preinit_turn_0(squadrons_start_full)
self.red.preinit_turn_0(squadrons_start_full)
# TODO: Check for overfull bases.
# We don't need to actually stream events for turn zero because we haven't given
# *any* state to the UI yet, so it will need to do a full draw once we do.
self.initialize_turn(GameUpdateEvents())
def save_last_turn_state(self) -> None:
self.save_manager.save_last_turn()
def pass_turn(self, no_action: bool = False) -> None:
"""Ends the current turn and initializes the new turn.
@@ -332,6 +342,12 @@ class Game:
from .server import EventStream
from .sim import GameUpdateEvents
if no_action:
# Only save the last turn state if the turn was skipped. Otherwise, we'll
# end up saving the game after we've already applied the results, making
# this useless...
self.save_manager.save_last_turn()
events = GameUpdateEvents()
logging.info("Pass turn")
@@ -343,8 +359,7 @@ class Game:
EventStream.put_nowait(events)
# Autosave progress
persistency.autosave(self)
self.save_manager.save_start_of_turn()
def check_win_loss(self) -> TurnState:
player_airbases = {
@@ -367,7 +382,10 @@ class Game:
self.red.bullseye = Bullseye(player_cp.position)
def initialize_turn(
self, events: GameUpdateEvents, for_red: bool = True, for_blue: bool = True
self,
events: GameUpdateEvents,
for_red: bool = True,
for_blue: bool = True,
) -> None:
"""Performs turn initialization for the specified players.
@@ -419,17 +437,9 @@ class Game:
# Plan Coalition specific turn
if for_blue:
self.blue.initialize_turn()
self.blue.initialize_turn(self.turn == 0)
if for_red:
self.red.initialize_turn()
# Plan GroundWar
self.ground_planners = {}
for cp in self.theater.controlpoints:
if cp.has_frontline:
gplanner = GroundPlanner(cp, self)
gplanner.plan_groundwar()
self.ground_planners[cp.id] = gplanner
self.red.initialize_turn(self.turn == 0)
# Update cull zones
with logged_duration("Computing culling positions"):

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import logging
from collections import defaultdict
from datetime import datetime
from typing import Optional, TYPE_CHECKING
from game.theater import ControlPoint
@@ -52,7 +53,7 @@ class GroundUnitOrders:
pending_units = 0
return pending_units
def process(self, game: Game) -> None:
def process(self, game: Game, now: datetime) -> None:
coalition = game.coalition_for(self.destination.captured)
ground_unit_source = self.find_ground_unit_source(game)
if ground_unit_source is None:
@@ -95,15 +96,20 @@ class GroundUnitOrders:
"still tried to transfer units to there"
)
ground_unit_source.base.commission_units(units_needing_transfer)
self.create_transfer(coalition, ground_unit_source, units_needing_transfer)
self.create_transfer(
coalition, ground_unit_source, units_needing_transfer, now
)
def create_transfer(
self,
coalition: Coalition,
source: ControlPoint,
units: dict[GroundUnitType, int],
now: datetime,
) -> None:
coalition.transfers.new_transfer(TransferOrder(source, self.destination, units))
coalition.transfers.new_transfer(
TransferOrder(source, self.destination, units), now
)
def find_ground_unit_source(self, game: Game) -> Optional[ControlPoint]:
# This is running *after* the turn counter has been incremented, so this is the

View File

@@ -1,9 +1,9 @@
from __future__ import annotations
from collections import defaultdict
import itertools
import logging
import pickle
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from typing import Iterator
@@ -13,18 +13,18 @@ import yaml
from dcs import Point
from dcs.unitgroup import StaticGroup
from game import persistency
from game import persistence
from game.data.groups import GroupRole
from game.layout.layout import (
AntiAirLayout,
BuildingLayout,
DefensesLayout,
GroundForceLayout,
LayoutUnit,
NavalLayout,
TgoLayout,
TgoLayoutGroup,
TgoLayoutUnitGroup,
LayoutUnit,
AntiAirLayout,
BuildingLayout,
NavalLayout,
GroundForceLayout,
DefensesLayout,
)
from game.layout.layoutmapping import LayoutMapping
from game.profiling import logged_duration
@@ -63,7 +63,7 @@ class LayoutLoader:
"""This will load all pre-loaded layouts from a pickle file.
If pickle can not be loaded it will import and dump the layouts"""
# We use a pickle for performance reasons. Importing takes many seconds
file = Path(persistency.base_path()) / LAYOUT_DUMP
file = Path(persistence.base_path()) / LAYOUT_DUMP
if file.is_file():
# Load from pickle if existing
with file.open("rb") as f:
@@ -106,7 +106,7 @@ class LayoutLoader:
self._dump_templates()
def _dump_templates(self) -> None:
file = Path(persistency.base_path()) / LAYOUT_DUMP
file = Path(persistence.base_path()) / LAYOUT_DUMP
dump = (VERSION, self._layouts)
with file.open("wb") as fdata:
pickle.dump(dump, fdata)

View File

@@ -16,7 +16,7 @@ def init_logging(version: str) -> None:
log_config = resources / "default_logging.yaml"
if (custom_log_config := resources / "logging.yaml").exists():
log_config = custom_log_config
with log_config.open() as log_file:
with log_config.open(encoding="utf-8") as log_file:
logging.config.dictConfig(yaml.safe_load(log_file))
logging.info(f"DCS Liberation {version}")

View File

@@ -24,6 +24,7 @@ from dcs.unitgroup import FlyingGroup
from game.ato import Flight, FlightType
from game.ato.flightplans.aewc import AewcFlightPlan
from game.ato.flightplans.shiprecoverytanker import RecoveryTankerFlightPlan
from game.ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan
@@ -246,7 +247,10 @@ class AircraftBehavior:
def configure_refueling(self, group: FlyingGroup[Any], flight: Flight) -> None:
group.task = Refueling.name
if not isinstance(flight.flight_plan, TheaterRefuelingFlightPlan):
if not (
isinstance(flight.flight_plan, TheaterRefuelingFlightPlan)
or isinstance(flight.flight_plan, RecoveryTankerFlightPlan)
):
logging.error(
f"Cannot configure racetrack refueling tasks for {flight} because it "
"does not have an racetrack refueling flight plan."

View File

@@ -26,6 +26,7 @@ from game.settings import Settings
from game.theater.controlpoint import (
Airfield,
ControlPoint,
Fob,
)
from game.unitmap import UnitMap
from .aircraftpainter import AircraftPainter
@@ -177,6 +178,15 @@ class AircraftGenerator:
self.mission_data,
dynamic_runways,
self.use_client,
self.unit_map,
).configure()
)
wpt = group.waypoint("LANDING")
if flight.is_helo and isinstance(flight.arrival, Fob) and wpt:
hpad = self.helipads[flight.arrival].units.pop(0)
wpt.helipad_id = hpad.id
wpt.link_unit = hpad.id
self.helipads[flight.arrival].units.append(hpad)
return group

View File

@@ -12,19 +12,18 @@ from dcs.unitgroup import FlyingGroup
from game.ato import Flight, FlightType
from game.callsigns import callsign_for_support_unit
from game.data.weapons import Pylon, WeaponType as WeaponTypeEnum
from game.missiongenerator.missiondata import MissionData, AwacsInfo, TankerInfo
from game.missiongenerator.lasercoderegistry import LaserCodeRegistry
from game.missiongenerator.logisticsgenerator import LogisticsGenerator
from game.missiongenerator.missiondata import AwacsInfo, MissionData, TankerInfo
from game.radio.radios import RadioFrequency, RadioRegistry
from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage
from game.runways import RunwayData
from game.squadrons import Pilot
from game.unitmap import UnitMap
from .aircraftbehavior import AircraftBehavior
from .aircraftpainter import AircraftPainter
from .flightdata import FlightData
from .waypoints import WaypointGenerator
from ...ato.flightplans.aewc import AewcFlightPlan
from ...ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan
if TYPE_CHECKING:
from game import Game
@@ -44,6 +43,7 @@ class FlightGroupConfigurator:
mission_data: MissionData,
dynamic_runways: dict[str, RunwayData],
use_client: bool,
unit_map: UnitMap,
) -> None:
self.flight = flight
self.group = group
@@ -56,6 +56,7 @@ class FlightGroupConfigurator:
self.mission_data = mission_data
self.dynamic_runways = dynamic_runways
self.use_client = use_client
self.unit_map = unit_map
def configure(self) -> FlightData:
AircraftBehavior(self.flight.flight_type).apply_to(self.flight, self.group)
@@ -78,14 +79,18 @@ class FlightGroupConfigurator:
if self.flight.flight_type in [
FlightType.TRANSPORT,
FlightType.AIR_ASSAULT,
] and self.game.settings.plugin_option("ctld"):
] and self.game.lua_plugin_manager.is_plugin_enabled("ctld"):
transfer = None
if self.flight.flight_type == FlightType.TRANSPORT:
coalition = self.game.coalition_for(player=self.flight.blue)
transfer = coalition.transfers.transfer_for_flight(self.flight)
self.mission_data.logistics.append(
LogisticsGenerator(
self.flight, self.group, self.mission, self.game.settings, transfer
self.flight,
self.group,
self.mission,
self.game.lua_plugin_manager,
transfer,
).generate_logistics()
)
@@ -93,10 +98,10 @@ class FlightGroupConfigurator:
self.flight,
self.group,
self.mission,
self.game.conditions.start_time,
self.time,
self.game.settings,
self.mission_data,
self.unit_map,
).create_waypoints()
return FlightData(
@@ -144,7 +149,7 @@ class FlightGroupConfigurator:
def register_air_support(self, channel: RadioFrequency) -> None:
callsign = callsign_for_support_unit(self.group)
if isinstance(self.flight.flight_plan, AewcFlightPlan):
if self.flight.flight_type is FlightType.AEWC:
self.mission_data.awacs.append(
AwacsInfo(
group_name=str(self.group.name),
@@ -156,7 +161,7 @@ class FlightGroupConfigurator:
blue=self.flight.departure.captured,
)
)
elif isinstance(self.flight.flight_plan, TheaterRefuelingFlightPlan):
elif self.flight.flight_type is FlightType.REFUELING:
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y, TacanUsage.AirToAir)
self.mission_data.tankers.append(
TankerInfo(
@@ -165,8 +170,8 @@ class FlightGroupConfigurator:
variant=self.flight.unit_type.name,
freq=channel,
tacan=tacan,
start_time=self.flight.flight_plan.patrol_start_time,
end_time=self.flight.flight_plan.patrol_end_time,
start_time=self.flight.flight_plan.mission_begin_on_station_time,
end_time=self.flight.flight_plan.mission_departure_time,
blue=self.flight.departure.captured,
)
)

View File

@@ -230,16 +230,22 @@ class FlightGroupSpawner:
def _generate_at_cp_helipad(self, name: str, cp: ControlPoint) -> FlyingGroup[Any]:
try:
helipad = self.helipads[cp]
except IndexError as ex:
except IndexError:
raise NoParkingSlotError()
group = self._generate_at_group(name, helipad)
if self.start_type is not StartType.COLD:
if self.start_type is StartType.WARM:
group.points[0].type = "TakeOffParkingHot"
hpad = helipad.units[0]
for i in range(self.flight.count):
group.units[i].position = helipad.units[i].position
group.units[i].heading = helipad.units[i].heading
group.units[i].position = hpad.position
group.units[i].heading = hpad.heading
# pydcs has just `parking_id = None`, so mypy thinks str is invalid. Ought
# to fix pydcs, but that's not the kind of change we want to pull into the
# 6.1 branch, and frankly we should probably just improve pydcs's handling
# of FARPs instead.
group.units[i].parking_id = str(i + 1) # type: ignore
return group
def dcs_start_type(self) -> DcsStartType:

View File

@@ -1,25 +0,0 @@
from dcs import Mission
from dcs.action import SetFlag
from dcs.condition import TimeAfter
from dcs.task import ControlledTask
from dcs.triggers import TriggerOnce, Event
from game.ato import Package
def create_stop_orbit_trigger(
orbit: ControlledTask, package: Package, mission: Mission, elapsed: int
) -> None:
orbit.stop_if_user_flag(id(package), True)
orbits = [
x
for x in mission.triggerrules.triggers
if x.comment == f"StopOrbit{id(package)}"
]
if not any(orbits):
stop_trigger = TriggerOnce(Event.NoEvent, f"StopOrbit{id(package)}")
stop_condition = TimeAfter(elapsed)
stop_action = SetFlag(id(package))
stop_trigger.add_condition(stop_condition)
stop_trigger.add_action(stop_action)
mission.triggerrules.triggers.append(stop_trigger)

View File

@@ -4,7 +4,6 @@ from dcs.point import MovingPoint
from dcs.task import ControlledTask, OptFormation, OrbitAction
from game.ato.flightplans.loiter import LoiterFlightPlan
from ._helper import create_stop_orbit_trigger
from .pydcswaypointbuilder import PydcsWaypointBuilder
@@ -23,10 +22,6 @@ class HoldPointBuilder(PydcsWaypointBuilder):
return
push_time = self.flight.flight_plan.push_time
self.waypoint.departure_time = push_time
elapsed = int((push_time - self.elapsed_mission_time).total_seconds())
loiter.stop_after_time(elapsed)
# What follows is some code to cope with the broken 'stop after time' condition
create_stop_orbit_trigger(loiter, self.package, self.mission, elapsed)
# end of hotfix
loiter.stop_after_time(int((push_time - self.now).total_seconds()))
waypoint.add_task(loiter)
waypoint.add_task(OptFormation.finger_four_close())

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from datetime import timedelta
from datetime import datetime
from typing import Any, Iterable, Union
from dcs import Mission
@@ -12,6 +12,7 @@ from game.ato import Flight, FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType
from game.missiongenerator.missiondata import MissionData
from game.theater import MissionTarget, TheaterUnit
from game.unitmap import UnitMap
TARGET_WAYPOINTS = (
FlightWaypointType.TARGET_GROUP_LOC,
@@ -27,16 +28,20 @@ class PydcsWaypointBuilder:
group: FlyingGroup[Any],
flight: Flight,
mission: Mission,
elapsed_mission_time: timedelta,
now: datetime,
mission_data: MissionData,
unit_map: UnitMap,
generated_waypoint_idx: int,
) -> None:
self.waypoint = waypoint
self.group = group
self.package = flight.package
self.flight = flight
self.mission = mission
self.elapsed_mission_time = elapsed_mission_time
self.now = now
self.mission_data = mission_data
self.unit_map = unit_map
self.generated_waypoint_idx = generated_waypoint_idx
def build(self) -> MovingPoint:
waypoint = self.group.add_waypoint(
@@ -65,10 +70,10 @@ class PydcsWaypointBuilder:
def add_tasks(self, waypoint: MovingPoint) -> None:
pass
def set_waypoint_tot(self, waypoint: MovingPoint, tot: timedelta) -> None:
def set_waypoint_tot(self, waypoint: MovingPoint, tot: datetime) -> None:
self.waypoint.tot = tot
if not self._viggen_client_tot():
waypoint.ETA = int((tot - self.elapsed_mission_time).total_seconds())
waypoint.ETA = int((tot - self.now).total_seconds())
waypoint.ETA_locked = True
waypoint.speed_locked = False

View File

@@ -12,7 +12,6 @@ from dcs.task import (
from game.ato import FlightType
from game.ato.flightplans.patrolling import PatrollingFlightPlan
from ._helper import create_stop_orbit_trigger
from .pydcswaypointbuilder import PydcsWaypointBuilder
@@ -57,12 +56,8 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
racetrack = ControlledTask(orbit)
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)
loiter_duration = flight_plan.patrol_end_time - self.elapsed_mission_time
elapsed = int(loiter_duration.total_seconds())
racetrack.stop_after_time(elapsed)
# What follows is some code to cope with the broken 'stop after time' condition
create_stop_orbit_trigger(racetrack, self.package, self.mission, elapsed)
# end of hotfix
loiter_duration = flight_plan.patrol_end_time - self.now
racetrack.stop_after_time(int(loiter_duration.total_seconds()))
waypoint.add_task(racetrack)
def configure_refueling_actions(self, waypoint: MovingPoint) -> None:

View File

@@ -0,0 +1,66 @@
from dcs.point import MovingPoint
from dcs.task import ActivateBeaconCommand, RecoveryTanker, Tanker
from game.ato import FlightType
from game.utils import feet, knots
from .pydcswaypointbuilder import PydcsWaypointBuilder
class RecoveryTankerBuilder(PydcsWaypointBuilder):
def add_tasks(self, waypoint: MovingPoint) -> None:
assert self.flight.flight_type == FlightType.REFUELING
# Tanker task required in conjunction with RecoveryTanker task.
# See link below for details.
# https://github.com/dcs-liberation/dcs_liberation/issues/2771
waypoint.add_task(Tanker())
group_id = self._get_carrier_group_id()
speed = knots(250).meters_per_second
altitude = feet(6000).meters
# Last waypoint has index of 1.
# Give the tanker a end condition of the last carrier waypoint.
# If the carrier ever gets more than one waypoint this approach needs to change.
last_waypoint = 2
recovery_tanker = RecoveryTanker(group_id, speed, altitude, last_waypoint)
waypoint.add_task(recovery_tanker)
self.configure_tanker_tacan(waypoint)
def _get_carrier_group_id(self) -> int:
name = self.package.target.name
carrier_position = self.package.target.position
theater_objects = self.unit_map.theater_objects
for key, value in theater_objects.items():
# Check name and position in case there are multiple of same carrier.
if name in key and value.theater_unit.position == carrier_position:
return value.dcs_group_id
raise RuntimeError(
f"Could not find a carrier in the mission matching {name} at "
f"({carrier_position.x}, {carrier_position.y})"
)
def configure_tanker_tacan(self, waypoint: MovingPoint) -> None:
if self.flight.unit_type.dcs_unit_type.tacan:
tanker_info = self.mission_data.tankers[-1]
tacan = tanker_info.tacan
tacan_callsign = {
"Texaco": "TEX",
"Arco": "ARC",
"Shell": "SHL",
}.get(tanker_info.callsign)
waypoint.add_task(
ActivateBeaconCommand(
tacan.number,
tacan.band.value,
tacan_callsign,
bearing=True,
unit_id=self.group.units[0].id,
aa=True,
)
)

View File

@@ -1,9 +1,12 @@
from dcs.point import MovingPoint
from dcs.task import RefuelingTaskAction
from game.ato import FlightType
from .pydcswaypointbuilder import PydcsWaypointBuilder
class RefuelPointBuilder(PydcsWaypointBuilder):
def add_tasks(self, waypoint: MovingPoint) -> None:
waypoint.add_task(RefuelingTaskAction())
if self.package.has_flight_with_task(FlightType.REFUELING):
waypoint.add_task(RefuelingTaskAction())
return super().add_tasks(waypoint)

View File

@@ -3,13 +3,15 @@ import logging
from dcs.point import MovingPoint
from dcs.task import (
AttackGroup,
ControlledTask,
EngageGroup,
Expend,
OptECMUsing,
SwitchWaypoint,
WeaponType as DcsWeaponType,
)
from game.ato.flightstate import InFlight
from game.data.weapons import WeaponType
from game.theater import TheaterGroundObject
from .pydcswaypointbuilder import PydcsWaypointBuilder
@@ -45,8 +47,28 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
engage_task.params["groupAttack"] = True
engage_task.params["expend"] = Expend.All.value
waypoint.tasks.append(engage_task)
elif self.flight.loadout.has_weapon_of_type(WeaponType.DECOY):
# Special handling for DECOY weapon types:
# - Specify that DECOY weapon type is used in AttackGroup task so that
# the flight actually launches the decoy. See link below for details
# https://github.com/dcs-liberation/dcs_liberation/issues/2780
# - Set a stop condition of 120 seconds so that the flight does not continue
# press the engagement as a DCS limitation means the RTB on winchester
# does not work well with decoys. See link below for details.
# https://github.com/dcs-liberation/dcs_liberation/issues/2781
# This stop condition will allow the SwitchWaypoint task defined below
# to kick in.
attack_task = AttackGroup(
miz_group.id,
weapon_type=DcsWeaponType.Decoy,
group_attack=True,
expend=Expend.All,
)
attack_task_control = ControlledTask(attack_task)
attack_task_control.stop_after_duration(120)
waypoint.tasks.append(attack_task_control)
else:
# All non ARM types like Decoys will use the normal AttackGroup Task
# All non ARM and non DECOY types will use the normal AttackGroup Task
attack_task = AttackGroup(
miz_group.id,
weapon_type=DcsWeaponType.Guided,
@@ -58,3 +80,14 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
# Preemptively use ECM to better avoid getting swatted.
ecm_option = OptECMUsing(value=OptECMUsing.Values.UseIfDetectedLockByRadar)
waypoint.tasks.append(ecm_option)
# For DECOY type flights, setup a waypoint task to skip the target waypoint after
# the attack task is complete. This is achieved using a switch waypoint task from the
# INGRESS point to the SPLIT point. This tasking prevents the flights continuing to
# overfly the target. See link below for the details of this issue
# https://github.com/dcs-liberation/dcs_liberation/issues/2781
if self.flight.loadout.has_weapon_of_type(WeaponType.DECOY):
switch_waypoint_task = SwitchWaypoint(
self.generated_waypoint_idx, self.generated_waypoint_idx + 2
)
waypoint.tasks.append(switch_waypoint_task)

View File

@@ -1,8 +1,9 @@
import copy
from dcs import Point
from dcs.planes import B_17G, B_52H, Tu_22M3
from dcs.point import MovingPoint
from dcs.task import Bombing, OptFormation, WeaponType, Expend
from dcs.task import Bombing, Expend, OptFormation, WeaponType
from .pydcswaypointbuilder import PydcsWaypointBuilder
@@ -21,7 +22,7 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
if not targets:
return
center = copy.copy(targets[0].position)
center: Point = copy.copy(targets[0].position)
for target in targets[1:]:
center += target.position
center /= len(targets)

View File

@@ -17,17 +17,21 @@ from game.ato.flightstate import InFlight, WaitingForStart
from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.starttype import StartType
from game.missiongenerator.aircraft.waypoints.cargostop import CargoStopBuilder
from game.missiongenerator.aircraft.waypoints.recoverytanker import (
RecoveryTankerBuilder,
)
from game.missiongenerator.missiondata import MissionData
from game.settings import Settings
from game.unitmap import UnitMap
from game.utils import pairwise
from .baiingress import BaiIngressBuilder
from .landingzone import LandingZoneBuilder
from .casingress import CasIngressBuilder
from .deadingress import DeadIngressBuilder
from .default import DefaultWaypointBuilder
from .holdpoint import HoldPointBuilder
from .joinpoint import JoinPointBuilder
from .landingpoint import LandingPointBuilder
from .landingzone import LandingZoneBuilder
from .ocaaircraftingress import OcaAircraftIngressBuilder
from .ocarunwayingress import OcaRunwayIngressBuilder
from .pydcswaypointbuilder import PydcsWaypointBuilder, TARGET_WAYPOINTS
@@ -46,18 +50,18 @@ class WaypointGenerator:
flight: Flight,
group: FlyingGroup[Any],
mission: Mission,
turn_start_time: datetime,
time: datetime,
settings: Settings,
mission_data: MissionData,
unit_map: UnitMap,
) -> None:
self.flight = flight
self.group = group
self.mission = mission
self.elapsed_mission_time = time - turn_start_time
self.time = time
self.settings = settings
self.mission_data = mission_data
self.unit_map = unit_map
def create_waypoints(self) -> tuple[timedelta, list[FlightWaypoint]]:
for waypoint in self.flight.points:
@@ -76,7 +80,7 @@ class WaypointGenerator:
# us, but we do need to configure the tasks for it so that mid-
# mission aircraft starting at a waypoint with tasks behave
# correctly.
self.builder_for_waypoint(point).add_tasks(self.group.points[0])
self.builder_for_waypoint(point, 1).add_tasks(self.group.points[0])
if not self.flight.state.has_passed_waypoint(point):
filtered_points.append(point)
else:
@@ -106,7 +110,10 @@ class WaypointGenerator:
]
for idx, point in enumerate(filtered_points):
self.builder_for_waypoint(point).build()
# We add 2 to idx to get the generated waypoint index as
# 1) pydcs seems to decrement the index by 1 and
# 2) DCS starts the first waypoint at index 1 as 0 is the starting position
self.builder_for_waypoint(point, idx + 2).build()
# Set here rather than when the FlightData is created so they waypoints
# have their TOTs and fuel minimums set. Once we're more confident in our fuel
@@ -116,7 +123,9 @@ class WaypointGenerator:
self._estimate_min_fuel_for(waypoints)
return mission_start_time, waypoints
def builder_for_waypoint(self, waypoint: FlightWaypoint) -> PydcsWaypointBuilder:
def builder_for_waypoint(
self, waypoint: FlightWaypoint, generated_waypoint_index: int
) -> PydcsWaypointBuilder:
builders = {
FlightWaypointType.INGRESS_BAI: BaiIngressBuilder,
FlightWaypointType.INGRESS_CAS: CasIngressBuilder,
@@ -135,6 +144,7 @@ class WaypointGenerator:
FlightWaypointType.PICKUP_ZONE: LandingZoneBuilder,
FlightWaypointType.DROPOFF_ZONE: LandingZoneBuilder,
FlightWaypointType.REFUEL: RefuelPointBuilder,
FlightWaypointType.RECOVERY_TANKER: RecoveryTankerBuilder,
FlightWaypointType.CARGO_STOP: CargoStopBuilder,
}
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
@@ -143,8 +153,10 @@ class WaypointGenerator:
self.group,
self.flight,
self.mission,
self.elapsed_mission_time,
self.time,
self.mission_data,
self.unit_map,
generated_waypoint_index,
)
def _estimate_min_fuel_for(self, waypoints: list[FlightWaypoint]) -> None:
@@ -174,12 +186,29 @@ class WaypointGenerator:
a.min_fuel = min_fuel
def set_takeoff_time(self, waypoint: FlightWaypoint) -> timedelta:
force_delay = False
if isinstance(self.flight.state, WaitingForStart):
delay = self.flight.state.time_remaining(self.time)
elif (
# The first two clauses capture the flight states that we want to adjust. We
# don't want to delay any flights that are already in flight or on the
# runway.
not self.flight.state.in_flight
and self.flight.state.spawn_type is not StartType.RUNWAY
and self.flight.departure.is_fleet
and not self.flight.client_count
):
# https://github.com/dcs-liberation/dcs_liberation/issues/1309
# Without a delay, AI aircraft will be spawned on the sixpack, which other
# AI planes of course want to taxi through, deadlocking the carrier deck.
# Delaying AI carrier deck spawns by one second for some reason causes DCS
# to spawn those aircraft elsewhere, avoiding the traffic jam.
delay = timedelta(seconds=1)
force_delay = True
else:
delay = timedelta()
if self.should_delay_flight():
if force_delay or self.should_delay_flight():
if self.should_activate_late():
# Late activation causes the aircraft to not be spawned
# until triggered.

View File

@@ -15,7 +15,7 @@ from dcs.task import (
)
from dcs.unittype import UnitType
from game.ato.ai_flight_planner_db import AEWC_CAPABLE
from game.ato import FlightType
from game.callsigns import callsign_for_support_unit
from game.naming import namegen
from game.radio.radios import RadioRegistry
@@ -165,7 +165,7 @@ class AirSupportGenerator:
possible_awacs = [
a
for a in self.game.faction_for(player=True).aircrafts
if a in AEWC_CAPABLE
if a.capable_of(FlightType.AEWC)
]
if not possible_awacs:

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