417 Commits

Author SHA1 Message Date
MetalStormGhost
e9832b5d82 Super Étendard mod support v2.5.5 (#273)
* Implemented support for the VSN Super Étendard v2.5.5

* Updated changelog and renamed the banner.

---------

Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2024-03-16 14:55:50 +00:00
Starfire13
147136c41a Add PMC Russian Hard faction (#272) 2024-03-16 15:49:04 +01:00
MetalStormGhost
6204dd4e72 F9F Panther mod support (version v2.8.7.101) (#226)
* Added F9F Panther mod support (version v2.8.7.101). Still missing loadouts and icon+banner.

* Added missing import in pydcs_extensions/__init__.py

* Added a banner and icon for the F9F Panther, credit for the banner to Jim over at the Century Series Discord. VSN Century Series logo used with permission. Also added loadouts for the F9F Panther and fixed the mod settings for it.
2024-03-16 15:47:04 +01:00
Raffson
19daa1737e Drop single bombs on statics again 2024-03-10 22:55:00 +01:00
Raffson
0b40f872bf And now formatting -_- 2024-03-10 15:51:14 +01:00
Raffson
733e31c35a Fix mypy issue -_- 2024-03-10 15:49:27 +01:00
Raffson
fc9a1ad145 Use original trigger zone type for scenery targets 2024-03-10 14:52:28 +01:00
Raffson
94801f859a Formatting 2024-03-10 01:23:48 +01:00
Raffson
3cd1b2f80a Fine-tune target waypoints for Strike Eagle 2024-03-10 00:51:09 +01:00
Raffson
a9287809a3 Fix "White Flag #" strike target name 2024-03-09 23:25:50 +01:00
Raffson
ced97dff1e Transition to AGL below 5000ft 2024-03-09 22:33:02 +01:00
Raffson
c418d70d96 Lower minimum cruise & combat altitudes in doctrines 2024-03-09 22:33:02 +01:00
Raffson
1085cbbd9d Update Liberation's Patreon link 2024-03-09 22:33:02 +01:00
Raffson
c39d58f60b Add options: max mission distance for planes & helicopters 2024-03-09 22:33:01 +01:00
Raffson
f6985d9f70 Bring flight-plan to front if selected 2024-03-09 22:31:48 +01:00
Raffson
eb6afbdf02 Ignore range when manually using auto-create 2024-03-09 22:31:47 +01:00
Raffson
15218d0241 caniuse-lite update 2024-03-09 21:58:06 +01:00
Dan Albert
005bbcb812 Send flight plan paths to the back of the map.
This fixes the unusual case where the `interactive: false` property had
no effect, which would make it impossible to plan missions against UI
elements that were overflown by many flights (such as the front line).

As an added bonus, it looks a bit nicer.

This impacts the test in an odd way, but the cure for that is probably
rewriting the test to not use a mock now that we've figured out how to
do that.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3295.
2024-03-09 21:58:06 +01:00
Starfire13
dc3e7e790b Campaign updates for chopper escort autoplan support (#271) 2024-03-09 21:57:45 +01:00
Raffson
9303e1cb9e Configurable cruise & combat altitude + randomized offsets (phase 1) 2024-03-04 00:01:29 +01:00
Raffson
7158a5e60d Don't degrade flight-plan when adding waypoints 2024-03-03 13:43:33 +01:00
Raffson
bcf8ee9d42 Fine-tune release of parking slots 2024-03-03 12:51:27 +01:00
Starfire13
7a3ca43514 Update Grabthar's Hammer miz (#270) 2024-03-03 11:43:45 +01:00
Starfire13
707c0793a3 Update Grabthars_hammer.yaml to fix Hokum III name (#269) 2024-03-03 11:42:15 +01:00
Raffson
0591f74a11 Fine-tune DEAD ingress wpt-tasks 2024-03-03 02:23:42 +01:00
Raffson
d484e133ce Revert "Force SEAD into threat-range"
This reverts commit 61b2d7c68e.
2024-03-03 01:39:27 +01:00
Raffson
d2004e5b11 Clear parking slots when save is loaded 2024-03-03 00:27:20 +01:00
Raffson
2fdc9bbb2d Free parking slots when passing turn 2024-03-03 00:01:27 +01:00
Raffson
61b2d7c68e Force SEAD into threat-range 2024-03-02 23:17:46 +01:00
Raffson
aab8454fe6 Change formations at HOLD & SPLIT 2024-03-02 23:17:45 +01:00
Raffson
9f204d0334 Fix UI bug wrt cheat-options 2024-03-02 17:36:36 +01:00
Raffson
b4ba200f95 Buy/Replace TGOs for free at turn 0 2024-03-01 23:46:56 +01:00
Raffson
32f8d8d0b7 New cheat option: manage REDFOR's TGOs 2024-03-01 23:06:46 +01:00
Raffson
9f7c9eec14 Send new-flight events when cloning
Fix #268
2024-03-01 22:33:23 +01:00
Raffson
697aef2aef Import beacons 2024-03-01 22:11:41 +01:00
Raffson
95580a6cef Compatibility fix for lua5.4 2024-03-01 22:11:14 +01:00
Raffson
9e1a642eb2 Add option to force air-starts at Nevatim 2024-02-25 16:15:09 +01:00
Starfire13
93c5cb767e Exercise Bright Star - avoid Nevatim due to DCS bug (#267)
https://forum.dcs.world/topic/335545-29-nevatim-ramp-starts-still-bugged/
2024-02-25 14:04:03 +01:00
Starfire13
0f23a526eb Add CH-47 and UH-60 to Bluefor Modern (#266) 2024-02-25 14:01:02 +01:00
dependabot[bot]
7fb19649e1 Bump follow-redirects from 1.15.2 to 1.15.5 in /client (#265)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.5.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.5)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-25 14:40:19 +02:00
Starfire13
65b2db0ece Add files via upload (#264) 2024-02-25 11:26:26 +01:00
Raffson
4416463ec8 Formatting 2024-02-25 01:09:01 +01:00
Raffson
c887a35d0b Move options to doctrine page 2024-02-25 01:05:50 +01:00
Raffson
9cb7617352 Optionally jettison empty fuel tanks for AI 2024-02-25 00:57:15 +01:00
Raffson
e36e213b76 Selectable aircraft type in AutoCreateDialog
Resolves #165
2024-02-25 00:38:17 +01:00
Raffson
b737f4e00c Guard against nil values 2024-02-24 22:05:13 +01:00
Raffson
d3d4acd806 Indicate performance impact for DCS Dismounts 2024-02-24 14:30:21 +01:00
Raffson
0d1c292b23 Add note to changelog 2024-02-24 14:21:27 +01:00
Raffson
7e620acb4f Update changelog 2024-02-23 23:43:27 +01:00
Raffson
aadff131a3 Pydcs update 2024-02-23 23:43:27 +01:00
Raffson
68a09ea81b Fix horrible bug 2024-02-23 23:43:27 +01:00
Starfire13
f1920d3bad Add FCR to new loadout slot for Apache Blk II (#263) 2024-02-23 23:43:03 +01:00
holyorangejuice
e7796be586 WRL Campaign and Faction Update
Overhauls to 1.2 on missions. Removed small error in faction.
2024-02-20 18:10:32 +01:00
Raffson
37fc571fa1 Pydcs fix
Resolve #261
2024-02-19 21:16:42 +01:00
Raffson
a7ee431768 Bump fastapi + starlette 2024-02-19 19:53:48 +01:00
tmz42
f7b34d2d79 Update Retake the Falklands - Add F-14A, add strike targets. 2024-02-19 19:31:32 +01:00
Raffson
e5c44a1f16 Fix save-compat issue 2024-02-19 19:18:21 +01:00
Raffson
e4b67ffb8a ROE optimisations 2024-02-19 19:18:21 +01:00
Starfire13
f54993f579 Update Exercise Bright Star OPFOR faction and squadron distribution (#259) 2024-02-19 07:55:53 +01:00
Raffson
b9230b6fb6 Improve travel-speeds for helicopters 2024-02-18 23:31:53 +01:00
Raffson
dbd9776aff Fix friendly AI shooting at fires at front-line 2024-02-18 23:29:10 +01:00
Raffson
e7e43a95fb Initialize Join wpt in migrator 2024-02-18 23:29:10 +01:00
Raffson
96b1f7cc3a Pydcs updates 2024-02-18 15:56:06 +01:00
Raffson
e5d199f20b Use ingress/arrival for helicopter join/split wpts 2024-02-18 15:56:05 +01:00
Raffson
fc467b62e0 Add comment 2024-02-18 13:23:19 +01:00
Starfire13
84b61b271b Updates for all of Starfire's campaigns (#258) 2024-02-18 13:22:21 +01:00
Starfire13
6571db3001 Add Egypt faction for Exercise Bright Star + Combined USA faction for Final Countdown II (#257) 2024-02-18 13:16:44 +01:00
Starfire13
3d98aaaae0 Add files via upload (#256) 2024-02-18 11:35:56 +01:00
Starfire13
581df54fe2 Add files via upload (#255) 2024-02-18 11:35:27 +01:00
Starfire13
e362b15c02 Add files via upload (#254) 2024-02-18 11:35:02 +01:00
Starfire13
738b0dffc5 Add DEAD mission type to Blackshark and Blackshark III choppers and update loadouts (#253) 2024-02-18 10:31:22 +01:00
Starfire13
56899985cd Add DEAD mission type to Apache and update loadouts (#252) 2024-02-18 10:26:52 +01:00
Raffson
781d611f17 Fix ground-seeking AirLift 2024-02-18 01:10:30 +01:00
Raffson
d901df4fa0 Remove more dead code 2024-02-18 01:10:29 +01:00
Raffson
40a733e147 Fix win/loss not triggering when carriers are present 2024-02-18 01:10:29 +01:00
Raffson
6e4d1e9e5c Remove duplicate code...
Like... how did I miss this -_-
2024-02-17 17:41:56 +01:00
Raffson
47531cd9fa Avoid heli AirAssault seeking ground after ingress 2024-02-17 01:03:05 +01:00
Raffson
1395dcea71 Tweaks to Airlift transfer 2024-02-16 22:55:43 +01:00
Raffson
fd6d81e3ad Update MIST to 4.5.122 2024-02-16 22:07:50 +01:00
Raffson
4ba5fa4766 Addition to existing changelog entry 2024-02-16 21:15:01 +01:00
Raffson
ca0a100d9a Send events when closing AWCD mid-campaign
Should fix the issue where the map isn't properly updating after making changes to the AWCD
2024-02-16 21:12:12 +01:00
Raffson
08c007b7bf Callsign not used when flight starts mid-mission 2024-02-16 20:47:31 +01:00
Raffson
e0aeabb63a Use CTLD zones for drop-off 2024-02-16 18:54:49 +01:00
Raffson
d9479be12e Max out airbase_threat_range in scenic_merge 2024-02-12 00:06:58 +01:00
Raffson
3f5b93a5c1 Introduce AttackShips compound task 2024-02-12 00:04:13 +01:00
Raffson
05163d41a0 Adjust comment 2024-02-12 00:03:48 +01:00
Raffson
5dfba64ac3 Don't break out of procurement loop 2024-02-12 00:03:27 +01:00
Raffson
76a077d38f Fix wrong names of CTLD-zones at Wadi al Jandali 2024-02-11 17:24:55 +01:00
Raffson
caad080b01 Update changelog 2024-02-11 17:17:30 +01:00
Raffson
35f6cd8f17 Plan Anti-Ship against all ship-groups of TGO 2024-02-11 16:56:10 +01:00
Raffson
89aa0985bc Multi-task fallback during configuration 2024-02-11 14:54:56 +01:00
Starfire13
747ccae023 S3B tasks update (#249)
* Add files via upload

* Add files via upload

* Adjust Strike & Remove OCA/Runway

Strike ->10xMk82 for better effect
OCA/Runway removed because S-3B doesn't support Runway Attack. As long as we don't implement some system to use the carpet bombing task as an alternative (note that this would need markers for representing the ends of runways), Ground Attack is no good to us...

---------

Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2024-02-11 13:54:03 +00:00
Starfire13
06242a433c Update B-52 loadout after ED's recent rework of the aircraft (#250)
* Add files via upload

* Use Mk82s exclusively in OCA/Runway payload

Combining different types of dumb bombs isn't quite supported by the AI (yet)

---------

Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2024-02-11 12:12:41 +00:00
Raffson
14a0310d25 Prioritize degrading IADS 2024-02-10 20:42:01 +01:00
Raffson
80bf5149fc Correct name of referenced weight factor 2024-02-10 18:39:43 +01:00
Raffson
80d1ae0950 Adding spaces 2024-02-10 18:39:43 +01:00
Raffson
cb68ff0df3 Configurable back-end port 2024-02-10 18:39:42 +01:00
Raffson
63d05ea696 Preference option to prioritize custom Liberation payloads 2024-02-09 23:52:27 +01:00
Raffson
7c05e94619 Remove unused code 2024-02-09 20:13:12 +01:00
Raffson
f4987e05e4 Rollback PySide6 update
White squares reported by @Starfire13
2024-02-09 19:51:32 +01:00
Raffson
4c13704630 Fix error on shutdown
Mentioned by NoGoodNews as well as Starfire, once you open the logs an error would present itself as follows:
RuntimeError: The SignalInstance object was already deleted
2024-02-09 19:20:29 +01:00
Raffson
3e373b706f pydcs update
Solving a bug where a mission can't be deserialized with Link4
2024-02-04 23:38:24 +01:00
Raffson
5aefa4ae55 Fix keyboard modifiers + off-by-one bug 2024-02-04 23:25:14 +01:00
Raffson
7f92db75df Upgrade dependencies 2024-02-04 23:25:14 +01:00
Raffson
7a94db5404 Remove redundant code 2024-02-04 20:35:36 +01:00
tmz42
a30bfd2a45 tmz campaign updates (#239) 2024-02-04 14:57:50 +01:00
MetalStormGhost
33cfcd211a Removed IDF Mods Project Iron Dome mod artillery assets, since they are not included in the IDF Assets Pack mod which is currently supported in Retribution. 2024-02-01 23:29:18 +01:00
Raffson
0edcd31367 Fix desync wrt off-map flights 2024-01-28 19:20:34 +01:00
MetalStormGhost
ca98183e94 IDF Assets Pack support v1.1 (#233)
* Updated Irondome support to IDF Assets Pack V1.1, which includes the David's Sling.

* Added an Israel 2017 faction with the David's Sling.

* IDF Assets Pack air defence presets and assets are now correctly removed from the faction when the mod is disabled.

* Removed the Iron Dome mod rocket launchers:
- "9M22U - 122mm Grad"
- "9M27F - 229mm Uragan"
- "9M55F - 300mm Smerch"

These were added to the Iron Dome Mod V1.2 in order for the radar to recognize them and be able to intercept them (these are limitations of DCS), so new rockets were added. However, they don't exist in the IDF Assets Pack.

* IronDome to IDF-Assets migration

---------

Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2024-01-28 17:35:24 +00:00
Raffson
8ff6e5688a Pin black in workflow
New version of 'black' will most likely cause a lot of merge conflicts, thus let's pin the version for now until the Pretense generator is merged...
2024-01-28 17:39:47 +01:00
Raffson
dad5e7c146 Fix TOT-offset issues 2024-01-28 17:39:46 +01:00
Raffson
8d56e2d3bb Reinitialize flights upon abort 2024-01-28 14:44:17 +01:00
Raffson
a2494e467e Deepcopy TOT for cloned package 2024-01-28 00:06:34 +01:00
Raffson
8ae63796cf Increase buffer around landing-zone
This aims to specifically tackle the issue where AI helicopters tend to crash into the depot of the pickup-zone
Resolves #138
2024-01-21 22:36:00 +01:00
Raffson
6a002a7d43 Finish up kneeboard improvements
Resolve #106
2024-01-21 20:37:47 +01:00
Raffson
0b060d3110 De-spawn AI flights on RTB if start-type was set to In-Flight
Resolve #73
2024-01-21 18:40:33 +01:00
Raffson
bc26eb3f5e Fix naval aircraft not always returning to carrier 2024-01-21 13:36:38 +01:00
Raffson
d6026681ee Formatting 2024-01-20 22:29:56 +01:00
Raffson
9ab8e15dd1 Avoid incompatible mix of SR/TR/LN units
The call to 'add_or_update_force_group' would end up merging the force-groups, which in turn could cause SAM sites to have an SA-2 SR & TR with SA-6 launchers...
2024-01-20 22:28:32 +01:00
Raffson
febc901518 Refactor 2024-01-20 17:25:40 +01:00
Raffson
26066ef8f4 Adjust parameters for delaying ground-forces 2024-01-20 17:21:46 +01:00
Raffson
f548b27ac5 More accurate switch-wpt index for escort split-trigger 2024-01-20 16:56:49 +01:00
Raffson
181f8583d5 Despawn aircraft returning to off-map CPs 2024-01-20 16:56:48 +01:00
Raffson
e6e557c18a Fix some edge-cases wrt SetUnlimitedFuel 2024-01-20 16:56:35 +01:00
Raffson
a6a872eebb Formatting 2024-01-20 16:56:30 +01:00
tmz42
d07cb46741 Waypoint changes (F15E Strike Targets and Tomcat IP) (#236)
* Change indentation of register_special_waypoints (put out of loop)

* Added Strike Targets as F-15E Mission Target points (Set/Mission 1)

* Set up check for ASM : only if non-human flight lead. Add targets to the kneeboard.

* Generate multiple sets (i-e M2.1) for situations where the number of points is more than 8.

Added check condition to kneeboard (otherwise, may result in multiple writes).

* Change name of register_special_waypoints to register_special_strike_points

Add register_special_ingress_points method for special IPs and add to the appropriate classes

* Add changelog entry for Tomcat's IP wpt

* Avoid depending on client slots for special wpts injection

---------

Co-authored-by: tmz42 <thomas.monnzie@gmail.com>
Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2024-01-20 15:54:37 +00:00
tmz42
df3bd146da Add SA-10 Flat Lid-B TR to radar_db.py (#235) 2024-01-11 18:35:56 +01:00
Raffson
93ec459460 Fix bug introduced by AI unlimited fuel option
Since the unlimited fuel option would add the option before the threat-reaction option, this result into a false assertion when using EWJ
2024-01-07 15:04:50 +01:00
Raffson
6e96b4fbb0 Fix AWCD bugs mentioned by Drexyl 2024-01-06 21:06:27 +01:00
tmz42
79b1d949ce AI unlimited fuel initial implementation (#227)
* Unlimited fuel for AI flights. For player flights, included at startup, for AI flights with join/split, applied at join/split.

* Unlimited fuel for AI flights. For player flights, included at startup, for AI flights with join/split, applied at join/split.

* Corrected default value of ai_unlimited_fuel to False in configure_behavior

* ai_unlimited_fuel : set argument based on setting and simplify activation section

* AI Unlimited Fuel : enable at start, disable at racetrack start/join, enable at racetrack end/split

* Correct typing : bool to Optional[bool]

---------

Co-authored-by: tmz42 <thomas.monnzie@gmail.com>
2024-01-06 21:04:57 +01:00
MetalStormGhost
5ca12373d8 Improvements to disabled aircraft mod handling in Faction (#234) 2023-12-30 18:13:38 +01:00
MetalStormGhost
7e0125725c Swedish Military Assets Pack air defence presets are now correctly removed from the faction when the mod is disabled. 2023-12-30 10:51:38 +01:00
dependabot[bot]
5eb25e02a4 Bump @babel/traverse and @trivago/prettier-plugin-sort-imports (#231)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) to 7.23.2 and updates ancestor dependencies [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) and [@trivago/prettier-plugin-sort-imports](https://github.com/trivago/prettier-plugin-sort-imports). These dependencies need to be updated together.


Updates `@babel/traverse` from 7.20.13 to 7.23.2
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

Updates `@babel/traverse` from 7.17.3 to 7.23.2
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

Updates `@trivago/prettier-plugin-sort-imports` from 3.4.0 to 4.3.0
- [Release notes](https://github.com/trivago/prettier-plugin-sort-imports/releases)
- [Changelog](https://github.com/trivago/prettier-plugin-sort-imports/blob/main/CHANGELOG.md)
- [Commits](https://github.com/trivago/prettier-plugin-sort-imports/compare/v3.4.0...v4.3.0)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
- dependency-name: "@babel/traverse"
  dependency-type: indirect
- dependency-name: "@trivago/prettier-plugin-sort-imports"
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-29 17:00:45 +00:00
dependabot[bot]
435bac3847 Bump @adobe/css-tools from 4.3.1 to 4.3.2 in /client (#230)
Bumps [@adobe/css-tools](https://github.com/adobe/css-tools) from 4.3.1 to 4.3.2.
- [Changelog](https://github.com/adobe/css-tools/blob/main/History.md)
- [Commits](https://github.com/adobe/css-tools/commits)

---
updated-dependencies:
- dependency-name: "@adobe/css-tools"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-29 16:59:25 +00:00
dependabot[bot]
6e879b297e Bump axios from 1.3.2 to 1.6.0 in /client (#229)
Bumps [axios](https://github.com/axios/axios) from 1.3.2 to 1.6.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.3.2...v1.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-29 16:59:10 +00:00
dependabot[bot]
bf4f882214 Bump pyinstaller from 5.13.0 to 5.13.1 (#228)
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 5.13.0 to 5.13.1.
- [Release notes](https://github.com/pyinstaller/pyinstaller/releases)
- [Changelog](https://github.com/pyinstaller/pyinstaller/blob/develop/doc/CHANGES.rst)
- [Commits](https://github.com/pyinstaller/pyinstaller/compare/v5.13.0...v5.13.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-29 16:57:06 +00:00
Raffson
d0dec86a6d Add Load/Save for AirWingConfig #167 2023-12-29 17:50:05 +01:00
Raffson
96c7e20488 Remove line causing exception
Presumably the signature changed when migrating to PySide6
2023-12-29 17:40:43 +01:00
Raffson
35408f7a53 Re-initialize turn after AWCD changes if turn > 0 2023-12-29 17:38:24 +01:00
Raffson
62df761d9c Rename faction.aircrafts -> faction.all_aircraft 2023-12-29 15:20:00 +01:00
Raffson
9ff9fa5b69 Add AWCD mid-game cheat 2023-12-29 13:16:30 +01:00
Raffson
d6f586bbe3 Pydcs update 2023-12-29 13:16:30 +01:00
ingax01
e63520265c added missing weapon classes after SU-30 2.7.3b update (#225) 2023-12-29 11:55:00 +01:00
MetalStormGhost
4d4f4d1930 F-4B/C mod support v2.8.7.204 (#217)
* Rename     pydcs_extensions/f4/__init__.py (from pydcs_extensions/f4b/__init__.py)
Rename     pydcs_extensions/f4/f4b.py (from pydcs_extensions/f4b/f4b.py)

* Rename     pydcs_extensions/f4/f4.py (from pydcs_extensions/f4/f4b.py)

* Support for the updated F-4B/C mod

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.

* F-4 mod support update to 2.8.7.201

* Updated QGeneratorSettings.py and the changelog.

* Updated F-4B and F-4C loadouts.

* Added the Phantom to some additional factions.

* Changelog updated: Updated support for F-4B/C Phantom mod to 2.8.7.204

The pydcs export for the latest patch was identical, which means it's also supported.

* Migrator change for renamed F-4B/C folder

---------

Co-authored-by: Nosajthedevil <78634843+Nosajthedevil@users.noreply.github.com>
Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2023-12-28 12:50:24 +01:00
Raffson
387f72ace2 Adjust clusterWeaps table to match clusterDamage table
Should fix script errors reported by users
2023-12-17 18:37:21 +01:00
Raffson
93abee2e35 Fix AttributeError when enabling cheats in campaign setup 2023-12-17 18:37:20 +01:00
Raffson
94580bad88 Add SHORAD as SAM & AAA as EWR 2023-12-17 18:37:20 +01:00
Raffson
b35e0c26d6 Fix wrong name 2023-12-17 17:37:15 +01:00
Raffson
7d95e3e2fe Add new units to skynet script 2023-12-17 17:37:14 +01:00
Raffson
9c40506728 Add units to mobile-SAM plugin-options 2023-12-17 17:36:57 +01:00
MetalStormGhost
60859d39c1 Fixed the Channel landmap. 2023-12-01 21:14:36 +01:00
MetalStormGhost
8740df65c4 F/A-18E/F Super Hornet AI Tanker mod support (#220)
* Added F/A-18E/F Super Hornet AI Tanker mod support (Chiller Juice Studios SuperBug Tanker AI version 1.1).

* Updated F/A-18E/F Super Hornet AI Tanker mod support (Chiller Juice Studios SuperBug Tanker AI) to version 1.2.

* F/A-18E/F Super Hornet AI Tanker (version 1.2) support merged to Retribution.

* Updated the pydcs extension to version 1.4.
2023-11-26 20:16:11 +01:00
tmz42
26b15c66b8 Small campaign alterations - Retake The Falklands and Scenic Merge 2023-11-26 20:13:02 +01:00
tmz42
04ac3d51b3 Added US Navy Squadrons Bulls and Swordsmen (#221)
Co-authored-by: tmz42 <thomas.monnzie@gmail.com>
2023-11-25 13:18:54 +02:00
Raffson
1c609e1b70 Formatting 2023-11-19 18:13:46 +01:00
Raffson
5de863fbb8 Update F-15ESE payloads 2023-11-19 16:19:17 +01:00
Raffson
11d8f139f8 Fix mypy complaining, again... 2023-11-19 15:32:11 +01:00
Raffson
65947129f0 Correct FAC callsign for JTAC 2023-11-19 13:37:27 +01:00
Raffson
d85d73a6d0 pydcs mypy fix 2023-11-19 01:42:36 +01:00
Raffson
28e3c3d817 mypy fix 2023-11-19 01:38:04 +01:00
Raffson
9907432a0a Add sunrise-sunset times on kneeboard 2023-11-18 23:46:38 +01:00
Raffson
ade81f4548 Ability to set callsigns 2023-11-18 23:06:56 +01:00
Raffson
aaf23d52bc pydcs follow-up for callsigns 2023-11-18 22:52:18 +01:00
Raffson
b5dd2767a7 pydcs update 2023-11-18 21:55:50 +01:00
Raffson
5043a1b8ac Fix wrong check 2023-11-18 20:32:06 +01:00
Raffson
cade8f4a19 Add lost weapons back to explTable 2023-11-13 18:11:32 +01:00
Raffson
82e2f1c2b5 Make text selectable in Settings window 2023-11-12 15:28:14 +01:00
Raffson
6cfa52740e Add laser guided rockets to Splash Damage 2023-11-12 12:59:33 +01:00
MetalStormGhost
26f83cf269 Splash Damage with Clusters and Ship Radar effects (#206)
* Splash Damage with Clusters and Ship Radar effects
Based on the Splash Damage 2.0 by wheelyjoe, FrozenDroid and spencershepard/GRIMM (21 December 2021 version)

New features:
 -damage boost for parked aircraft since they are hard to kill (DCS: Retribution OCA/Aircraft mission improvement)
 -cluster weapons support
 -helicopter gunship autocannon fragmentation effect support
 -napalm will now spawn fire on impact
 -ship radars might turn off when hit with anti-radiation missiles
 -BDA messages for splash damage

New features can be tweaked, turned on or off in the Retribution settings and/or in the script options.

* Updated changelog.md

* Added missing changelog entry from 29 May 2022 to Weapons_Damage_Updated.lua

* Integrate splashdamageclusters into original

* Fix 'nil' exceptions

* Remove Ghosti's original implementation

---------

Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2023-11-12 11:36:17 +00:00
tmz42
4cbadb1d81 Tripoint : from Incirlik is actually from Incirlik now 2023-11-11 12:41:58 +01:00
tmz42
f950230fb5 Deleted B-1Bs from RTF, added Satnav to faction file 2023-11-11 12:41:58 +01:00
MetalStormGhost
6384f2feb2 Updated QGeneratorSettings.py 2023-11-06 20:30:10 +01:00
MetalStormGhost
f648add735 Update A-4E-C mod support to version 2.2.0.
Updated Community A-4E-C mod version pydcs extension to 2.2.0 release.
2023-11-06 20:30:10 +01:00
tmz42
470cdd64d4 Added requested changes to the PR 2023-11-06 19:48:58 +01:00
tmz42
17ee2385f3 Retake the Falklands v1 : addec campaigns + factions 2023-11-06 19:48:58 +01:00
tmz42
25ee1dfc45 Tripoint Hostility update : added carrier spawn + flights from Turkey actually start from Incirlik 2023-11-06 19:48:58 +01:00
Thomas MONZIE
859af22d2c Faction correction - Removed SA10 Mobile + Castle Class 2023-10-25 17:37:29 +02:00
Thomas MONZIE
da1a820d45 Allied Sword - Minor error correction 2023-10-25 17:37:29 +02:00
Raffson
bc861380f0 Allow hiding IADS TGOs on MFD 2023-10-22 00:23:00 +02:00
Raffson
e3dcf8e547 Add new units 2023-10-21 21:27:42 +02:00
Raffson
9805c482c2 Pydcs update for unlimited fuel option 2023-10-21 21:27:42 +02:00
Raffson
7d314f8ee4 Lock waypoint speed if ETA is 0 & avoid negative ETAs 2023-10-21 21:27:41 +02:00
zhexu14
10aee31c7d address issue 3175 by introducing special divide by zero handling 2023-10-21 21:27:41 +02:00
Starfire13
6f1ee9561b Final Countdown 2 update for campaign inversion
Fixes incorrect airfield assignment when campaign is inverted
2023-10-21 21:27:41 +02:00
Raffson
8f531d247d Update pydcs for 2.9 support 2023-10-21 21:27:40 +02:00
Thomas MONZIE
f5bee7cc5d Sync Strike Eagle Squadrons from Liberation 2023-10-16 22:06:44 +02:00
Raffson
d8ce0a6c24 Revert "Fix mypy complaint"
This reverts commit b0047784dc.
2023-10-15 20:56:30 +02:00
Raffson
be634ab961 Revert "Update to Python3.11"
This reverts commit d6ba45a42b.
2023-10-15 20:53:03 +02:00
Raffson
b0047784dc Fix mypy complaint 2023-10-15 20:03:17 +02:00
Raffson
d6ba45a42b Update to Python3.11 2023-10-15 19:53:24 +02:00
Raffson
291822b5c8 PySide6 downgrade to 6.4.3
6.5.x seems to have some issues with QWebEngineView
2023-10-15 19:52:57 +02:00
Raffson
d3a56fa893 Remove unnecessary attribute 2023-10-15 17:32:32 +02:00
Raffson
7d347eee94 Fix traveltime & offsets 2023-10-15 13:32:11 +02:00
Raffson
bcdf2ad887 Fix wrong logic wrt hold WPT & air-assault 2023-10-15 12:57:28 +02:00
Raffson
22900407dd Format time for WPTs on map 2023-10-15 12:55:55 +02:00
Raffson
84c9098dba Add missing colon in departure time 2023-10-15 12:27:42 +02:00
Raffson
63e2c30e44 Fix SEAD Sweep not available at campaign start 2023-10-15 12:23:18 +02:00
Raffson
02ae5d9ad7 Remove faulty beacon in migrator 2023-10-14 20:11:08 +02:00
Raffson
91fb0cea58 pydcs update 2023-10-14 19:27:24 +02:00
Raffson
3c921198af PySide6 steamlining 2023-10-14 18:42:48 +02:00
Raffson
7a3c9a707f Fix external views not allowing F5 view 2023-10-14 16:02:39 +02:00
Raffson
3b319ca481 Update contributors list 2023-10-14 15:52:04 +02:00
Raffson
7adbe71475 Fix payload saver 2023-10-14 15:40:29 +02:00
Raffson
06f51573e1 Fix kneeboard notes bug 2023-10-14 15:17:52 +02:00
tmz42
b2c38f454f Correct Squadron sizes for Fuzzle's campaigns + new campaign 'Scenic Merge' (#212)
* Corrected Squadron sizes for Fuzzle's campaigns + Scenic Merge

* Scenic Merge - Set performance to an accurate 3

---------

Co-authored-by: Thomas MONZIE <thomas.monnzie@gmail.com>
Co-authored-by: tmz42 <tmz42@users.noreply.github.com>
2023-10-12 18:05:58 +00:00
Raffson
c9ed0352b7 Fix kneeboard bug preventing mission generation 2023-10-08 14:23:12 +02:00
Raffson
b8ef4fc726 Pipeline? 2023-10-08 01:43:25 +02:00
Raffson
394627e8d5 Pipeline! 2023-10-08 01:40:03 +02:00
Raffson
f513e27dc6 Pipeline 2023-10-08 01:34:54 +02:00
dependabot[bot]
fe6ff6a1ae Bump electron from 21.4.1 to 22.3.25 in /client
Bumps [electron](https://github.com/electron/electron) from 21.4.1 to 22.3.25.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v21.4.1...v22.3.25)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-08 01:14:45 +02:00
Raffson
88a5179653 Streamlining 2023-10-08 01:12:45 +02:00
Raffson
a357097c49 Update dependencies 2023-10-08 00:53:52 +02:00
Raffson
c4be00d11b Migrate to PySide6 2023-10-08 00:53:51 +02:00
Raffson
93b83f2bc8 Streamlining 2023-10-08 00:37:39 +02:00
Dan Albert
a2e98f485c Remove bingo estimates from FlightPlan.
This doesn't need to be a part of FlightPlan, and it's easier to test if
it isn't. Move it out and add the tests.

It's pretty misleading to allow this in the core of the flight plan code
anything. This is an extremely unreliable estimate for most aircraft so
it should be more clearly just for briefing purposes.
2023-10-07 23:08:25 +02:00
Raffson
e04f23bf15 Streamlining 2023-10-07 22:46:16 +02:00
Dan Albert
f11689f4cb 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-10-07 21:43:28 +02:00
Raffson
ea726bbf06 Streamlining 2023-10-07 21:43:27 +02:00
Dan Albert
59673e7911 Convert TOTs to datetime.
https://github.com/dcs-liberation/dcs_liberation/issues/1680
2023-10-07 21:09:25 +02:00
Starfire13
625c4f2dfb New version of Exercise Bright Star with carrier 2023-10-07 20:55:09 +02:00
Dan Albert
6a47e84699 Increase max distance for waypoint solver.
1000km isn't large enough in the case where there's an off-map spawn
that's a long way from the target, but still in range for aircraft like
the B-1. Double it, which for now is enough to fix the one pathological
case we know.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3156.
2023-10-07 20:55:09 +02:00
Dan Albert
14d3e743d4 Remove useless TravelTime class.
This is only called for real in one spot. The other callers should have
been deferring to the one real caller.
2023-10-07 20:55:08 +02:00
Dan Albert
dcaee350cf Remove dead code. 2023-10-07 19:11:40 +02:00
Dan Albert
d205e66fe4 Don't flag negative starts for active flights.
If the flight has already passed its start up time, this isn't a
negative start.
2023-10-07 19:01:11 +02:00
Raffson
62a40db9ea Streamlining 2023-10-07 18:53:42 +02:00
Dan Albert
75498fe061 Differentiate total time and travel time.
There's an ugly special case in flight simulation to handle hold points
because we don't differentiate between the total time between two
waypoints (which can include delays from actions like holding) and
travel time. Split those up and remove the special case.
2023-10-07 18:42:59 +02:00
Dan Albert
5ca344e87b Show the real front line bounds on the map.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1067.
2023-10-07 18:35:17 +02:00
Raffson
03d8448def Streamlining 2023-10-07 18:13:55 +02:00
Dan Albert
a2630fc75f Add a real CAS ingress point.
Putting the ingress point directly on one end of the FLOT means that AI
flights won't start searching and engaging targets until they reach that
point. If the front line has advanced toward the flight's departure
airfield, it might overfly targets on its way to the IP.

Instead, place an IP for CAS the same way we place any other IP. The AI
will fly to that and start searching from there.

This also:

* Removes the midpoint waypoint, since it didn't serve any real purpose
* Names the FLOT boundary waypoints for what they actually are

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2231.
2023-10-07 18:00:07 +02:00
Raffson
c50dcaaafb Streamlining 2023-10-07 18:00:07 +02:00
Dan Albert
3499ba5ffa Mark the AI-only F-15E more clearly. 2023-10-07 17:24:40 +02:00
Dan Albert
f5f11ff3ac Add display name property for unit types.
Unlike the variant ID, this can be changed without breaking save compat.
2023-10-07 17:24:17 +02:00
Dan Albert
0ec375ad89 Fix unit variants to actually allow variance.
This was always the intent, but apparently it wasn't implemented
correctly. All properties of the unit type can now be overridden per
variant.
2023-10-07 17:23:34 +02:00
Raffson
ee3bdf9fd7 Streamlining 2023-10-07 17:14:01 +02:00
Dan Albert
1760532168 Rename UnitType.name what it is: the variant ID.
This property affects safe compat because the ID is what gets preserved
in the save, but it's unfortunately also used as the display name, which
means changing the display name breaks save compat. It also prevents us
from changing display names without breaking faction definitions.

This is the first step in fixing that. The next is adding a separate
display_name property that can be updated without breaking either of
those.
2023-10-07 17:08:33 +02:00
Dan Albert
97ee6ba19f Force dumping debug info on recreate.
We need a way to debug successful solvers that's still targeting to a
specific flight. This will do for now.
2023-10-07 17:07:57 +02:00
Dan Albert
d96f7a267c Add test cases found by fuzzer. 2023-10-07 17:07:33 +02:00
Dan Albert
98f92f9ab2 Add fuzz testing for waypoint solvers.
This fuzz test generates random inputs for waypoint solvers to check if
they can find a solution. If they can't, the debug info for the solver
is dumped to the testcases directory. Another test loads those test
cases, creates a solver from them, and checks that a solution is found.
Obviously it won't be immediately, but it's a starting point for fixing
the bug and serves as a regression test afterward.
2023-10-07 17:07:33 +02:00
Dan Albert
8b04dd878d Migrate IP placement to WaypointSolver. 2023-10-07 17:07:32 +02:00
Dan Albert
643dafd2c8 Build common interface for waypoint geometry constraints.
This is a replacement for the existing "zone geometry" classes that are
currently used for choosing locations for IP, hold, and join points.
The older approach required the author to define the methods for
choosing locations at a rather low level using shapely APIs to merge or
mask geometries. Debug UIs had to be defined manually which was a great
deal of work. Worse, those debug UIs were only useable for *successful*
waypoint placement. If there was a bug in the solver (which was pretty
much unavoidable during development or tuning), it wasn't possible to
use the debug UI.

This new system adds a (very simple) geometric constraint solver to
allow the author to describe the requirements for a waypoint at a high
level. Each waypoint type will define a waypoint solver that defines one
or more waypoint strategies which will be tried in order. For example,
the IP solver might have the following strategies:

1. Safe IP
2. Threat tolerant IP
3. Unsafe IP
4. Safe backtracking IP
5. Unsafe backtracking IP

We prefer those in the order defined, but the preferred strategies won't
always have a valid solution. When that happens, the next one is tried.

The strategies define the constraints for the waypoint location. For
example, the safe IP strategy could be defined as (in pseudo code):

* At least 5 NM away from the departure airfield
* Not farther from the departure airfield than the target is
* Within 10 NM and 45 NM of the target (doctrine dependent)
* Safe
* Within the permissible region, select the point nearest the departure
  airfield

When a solver fails to find a solution using any strategy, debug
information is automatically written in a GeoJSON format which can be
viewed on geojson.io.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3085.
2023-10-07 17:07:21 +02:00
Dan Albert
bf879a6141 Add __str__ for Distance. 2023-10-07 15:55:05 +02:00
Dan Albert
fa1caa86d6 Add name to Doctrine. 2023-10-07 15:55:04 +02:00
Dan Albert
181ac27299 Note the ARA Veinticinco de Mayo in the changelog. 2023-10-07 15:52:15 +02:00
Nosajthedevil
313d8f5065 Add support for ARA Veinticinco de Mayo.
Includes an Argentina 1982 faction for testing purposes, although it's
sparse because of a lack of assets in DCS.

Note that the carrier is mispelled as the Vienticinco in the game.

Includes prerequisite pydcs update.

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

yaml to json
2023-10-07 15:52:14 +02:00
Dan Albert
ee1fa78ecd Add __str__ for Heading. 2023-10-07 15:49:21 +02:00
Raffson
60172d0e52 Streamlining 2023-10-07 15:23:29 +02:00
Starfire13
e514cb1397 Add Operation Noisy Cricket campaign. 2023-10-07 14:51:52 +02:00
Starfire13
c640a53190 Add BLU-107 to OCA/Runway loadout
I had originally opted for iron bombs for anti-runway as the AI sometimes miss with the Durandals because they release them from too high an altitude. But after using them for a while, I am finding they appear to do no worse than with iron bombs. So, might as well let them use the dedicated anti-runway weapon.
2023-10-07 14:51:52 +02:00
Dan Albert
6ef3bd5095 Stop gap fix for AI speed to nav points.
This isn't a great fix for the reason I mention in the comment, but it's
quick and actually is accurate since it looks like we don't actually
handle formation speeds correctly in most cases...

This is probably as "fixed" as this is going to get for now since most
of the flight planning code is in the process of being rewritten.

https://github.com/dcs-liberation/dcs_liberation/issues/3113
2023-10-07 14:43:19 +02:00
Dan Albert
47add7848f Always re-enable loadout UI for member 0. 2023-10-07 14:42:51 +02:00
Dan Albert
0b7acbc94e Fix synchronization of loadouts on change.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3111.
2023-10-07 14:42:51 +02:00
Raffson
04974c3c0b Add Mirage F1 to Iran 2015
yaml to json
2023-10-07 14:37:15 +02:00
Dan Albert
014ae5faf5 Configure target points for F-15E S4+.
We don't need explicit configuration of initial points. The plane
automatically configures any steerpoint immediately before a target
point as an initial point.

Target offset points and aim points have not been implemented because I
can't find any information the describes their intent.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3088.
2023-10-07 14:34:30 +02:00
Dan Albert
e25aac774f Handle TOT offsets for patrolling flight plans.
https://github.com/dcs-liberation/dcs_liberation/issues/3107
2023-10-07 14:33:04 +02:00
Dan Albert
c11b4149a0 Fix negative starts when changing TOT offsets. 2023-10-07 14:31:08 +02:00
Dan Albert
091d523516 Fix altering negative TOT offsets.
https://github.com/dcs-liberation/dcs_liberation/issues/3107
2023-10-07 14:31:04 +02:00
Dan Albert
2ab456b9c1 Add laser code support for the viper. 2023-10-07 14:14:34 +02:00
Dan Albert
bb36944a8a Remove unused config data from laser code yamls. 2023-10-07 14:13:53 +02:00
Dan Albert
62e43f1169 Clarify that the assigned code is for the TGP. 2023-10-07 14:13:52 +02:00
Dan Albert
dc22e0a577 Hide properties that have better controls.
The weapon laser codes can be set more easily from the weapon laser code
combo box. Setting the properties explicitly here will just cause
conflicts and annoying UI bugs. Hide those properties from the UI.
2023-10-07 14:13:52 +02:00
Dan Albert
51f578b9e3 Add UI for selecting weapon laser codes.
This makes it possible to have the right laser code set for hot start
aircraft that (typically) do not allow changing laser codes when the
engine is on.
2023-10-07 14:13:51 +02:00
Dan Albert
2a8f34ea25 Do not draw empty property rows. 2023-10-07 14:13:51 +02:00
Dan Albert
26a71f4f0e Add laser code property info for the Strike Eagle. 2023-10-07 14:11:09 +02:00
Dan Albert
a366aebbb2 Add laser code config parsing and prop generation. 2023-10-07 14:11:09 +02:00
Raffson
1ce8d9c2b7 Cleanup after cherry-pick 2023-10-07 14:08:53 +02:00
Dan Albert
17e539e524 Add UI for acquiring/releasing TGP laser codes. 2023-10-07 14:06:13 +02:00
Dan Albert
177f357492 Pre-allocate laser codes for FLOTs and flights. 2023-10-07 14:06:12 +02:00
Dan Albert
723e191f10 Create a checked, releasable type for laser codes.
The release behavior isn't used yet, but I'm working on pre-allocating
laser codes for front lines and flights to make it easier for players to
pick the laser codes for their weapons.
2023-10-07 14:03:36 +02:00
Raffson
cb6bffe3ec Split procurement settings for blue & red
Resolves #208
2023-10-07 13:53:29 +02:00
MetalStormGhost
1ebc289fb7 Added support for ground power truck types in farp_truck_types_for_country(). Aircraft which require ground power to start, when ground power trucks have been disabled for performance reasons, will now be warm started automatically. 2023-10-06 07:58:06 +02:00
MetalStormGhost
d271428f9d Implemented ground power trucks for ground spawns. Added settings to turn them off or on. 2023-10-06 07:58:06 +02:00
MetalStormGhost
54777a9045 Helicopter waypoint altitude configurable (#207)
* Helicopter waypoint altitude configurable

Added a new option in Settings: Helicopter waypoint altitude (feet AGL).
It sets the waypoint altitude for helicopters in feet AGL. In campaigns in more mountainous areas, you might want to increase this setting to avoid the AI flying into the terrain.

* black?

* Distinguish cruise/combat altitudes for helicopters

Also includes a refactor for WaypointBuilder so it doesn't need a coalition. It can already reference the coalition from the flight.

* Update changelog.md

---------

Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2023-10-02 18:54:21 +02:00
Raffson
226a171550 Fix missing argument exception 2023-10-02 17:50:55 +02:00
Raffson
227134d9c1 Fix default cargo_ship name 2023-10-02 17:42:14 +02:00
Raffson
b3f5afe36b Fix 'game not defined' exception 2023-10-01 22:37:17 +02:00
Raffson
2c6c2e5fbd Update Skynet with baleBaron's 'ActMobile' version
Resolve #205

TODO: have all SHORADs use the 'ActMobile' functionality
2023-10-01 22:01:52 +02:00
Raffson
d4eefd0b55 Streamlining 2023-10-01 21:56:30 +02:00
Dan Albert
110866cd34 Add missing LANTIRN clsid. 2023-10-01 20:11:37 +02:00
Dan Albert
279572ae09 Add tests for LaserCodeRegistry, clean up.
* Store a deque rather than an iterator so it can be pickled
* Remove mangling from staticmethod (and rename now that it's no longer
  a generator)
* Rename "get" to "alloc" to make the mutation clear
* Move to its own package (the changes I'm working on make this no
  longer mission generator specific)
* Remove useless exception class. It's never caught so the unique type
  isn't needed
2023-10-01 20:11:37 +02:00
Dan Albert
485229b92f Allow per pilot loadouts and properties.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3092.
2023-10-01 20:10:16 +02:00
Raffson
8e670e1a3c Formatting 2023-10-01 19:32:20 +02:00
Dan Albert
bce6a170b8 Improve UI for flight properties.
Use the new data from pydcs to improve the properties UI:

* Use human readable names
* Use appropriate control types
* Limit min and max values as appropriate for each property
* Show labels

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3090.
2023-10-01 19:28:52 +02:00
zhexu14
a0fdfa11e2 Fix anti-runway task generation for LGBs. 2023-10-01 19:26:24 +02:00
Dan Albert
7303c8fa20 Add cheats for destroying and repairing runways. 2023-10-01 19:26:23 +02:00
Dan Albert
855fa1347b Fix canceling transfers when the airbase is full.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2955.
2023-10-01 19:26:23 +02:00
Raffson
5758436dbf Move WW2 factions to WW2 transports.
Only for those that require the WW2 asset pack. There don't appear to be any free WW2 transports.

Co-Authored-By: Dan Albert <danalbert@google.com>
2023-10-01 19:26:22 +02:00
Dan Albert
a343bdef23 Allow factions to specify their cargo ship type.
https://github.com/dcs-liberation/dcs_liberation/issues/3039
2023-10-01 19:26:22 +02:00
zhexu14
d3b0ae30eb Allow more helicopters to operate from LHAs and CVs.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3072.
2023-10-01 19:26:22 +02:00
Dan Albert
2c7e877086 Always initialize IADS coalition lua tables.
These are read unconditionally, but were only initialized when the
coalition had nodes. When a coalition had no nodes, this caused a nil
access. It's unclear if that had any symptoms, but I expect at the very
least it would break the remainder of the script (so a non-functioning
blue IADS if the red IADS had no nodes).

There's a very small chance this is the culprit behind
https://github.com/dcs-liberation/dcs_liberation/issues/3073.
2023-10-01 19:26:21 +02:00
Dan Albert
12cdb8646c Improve IP selection near threat zone centers.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2754.
2023-10-01 19:26:21 +02:00
Dan Albert
f89ac52bf3 Clean up remaining Flight.from_cp users.
The preferred API for this has been `Flight.departure` for a while.
2023-10-01 19:26:20 +02:00
Raffson
5e858731d2 Change task for 'Freya' preset group to AAA 2023-10-01 17:59:28 +02:00
Raffson
30f6c14030 Fix bug in migrator 2023-10-01 17:21:57 +02:00
Raffson
78e07fec83 Add comment 2023-10-01 13:02:40 +02:00
Raffson
1cf871c150 Add radio config for Hercules 2023-10-01 12:56:13 +02:00
Raffson
8e821c50e5 Interpret integer radio_preset keys as radio_id
Allows for configuration of radios 3, 4, 5,...
2023-09-30 20:19:09 +02:00
Raffson
3024bd6479 Fix Hercules AirAssault exception 2023-09-30 19:37:16 +02:00
Raffson
105d1abba8 Try determining intra-flight modulation in squadron config 2023-09-30 18:34:55 +02:00
Raffson
3b7077e593 Use all modeled radio-bands in freq-selector 2023-09-30 17:46:28 +02:00
Raffson
d581af3b8a Remove flights with package not in ATO in migrator 2023-09-24 22:14:26 +02:00
Raffson
e8929467e0 Consolidate capture radius 2023-09-23 19:38:48 +02:00
Raffson
234dc52c7e Make AirAssault auto-plannable 2023-09-23 19:38:29 +02:00
Raffson
ea74471307 Avoid refuel waypoint if air wing can't plan refuel flight 2023-09-23 17:25:12 +02:00
Raffson
b870198281 Add Starfire's Final Countdown II campaign 2023-09-23 17:11:44 +02:00
Raffson
389037e6bf Don't scrub missions with unplannable escort types
The check for whether or not the escort is actually needed happens later, and thus the mission can still be scrubbed if it's too dangerous. However, missions should not be scrubbed when a particular escort type is unplannable, yet not needed. Only if the primary flight type is not plannable should the mission immediately be scrubbed
2023-09-23 16:51:26 +02:00
Raffson
167d048232 Fix bug in reserve freqs & tacan 2023-09-17 14:25:46 +02:00
Raffson
59871ac36c Only assign TACAN to compatible tankers 2023-09-17 14:25:46 +02:00
Raffson
bd3205a442 Update red squadrons for TbilisiGap 2023-09-17 13:02:32 +02:00
Raffson
00a2d77543 Update tasking for MiG-29G 2023-09-17 13:02:32 +02:00
Raffson
1b000e83df Fix "no-purchase" bug at turn 0 2023-09-17 13:02:32 +02:00
Raffson
3787ec3039 Add radio config for UH-60L 2023-09-17 11:45:01 +02:00
Raffson
e341599a11 Account for parking-slot size during retreat
Fix #129
2023-09-09 13:20:56 +02:00
Raffson
e50d49d7ba Ability to disable cargo-ships
Fix #76
2023-09-09 12:09:57 +02:00
Raffson
0cb3702eb9 Fix exception after new payload file was saved
When a new payload file would be saved, the subsequent flight would always throw an exception (until Retribution would be restarted) because the payload_cache wasn't updated until now.
2023-09-09 12:03:47 +02:00
Raffson
09749bc088 Use squadron radio_preset freq as flight freq if present 2023-09-08 20:38:17 +02:00
Raffson
0b152c201d Fix rounding errors for squadron's radio presets 2023-09-08 19:22:01 +02:00
Raffson
fe470ad9c6 Migrator fix 2023-09-08 17:17:14 +02:00
Raffson
d22bfd0804 Fix 'IndexError' exception 2023-09-08 17:17:14 +02:00
dependabot[bot]
1ba3f2fbb4 Bump @adobe/css-tools from 4.1.0 to 4.3.1 in /client (#202) 2023-09-04 00:54:44 +02:00
dependabot[bot]
688b7f4469 Bump certifi from 2023.5.7 to 2023.7.22 (#201) 2023-09-04 00:53:43 +02:00
Raffson
a8a37fa3e7 Add GroupTask to TGO for map filtering
Resolves #21
2023-09-03 23:41:15 +02:00
Raffson
b7bc9efb52 Remove unused variable 2023-09-03 19:00:44 +02:00
Raffson
74cd3a2627 Remove debugging code 2023-09-03 17:37:41 +02:00
Raffson
e4cb0f7c28 Release parking slots when capture occurs 2023-09-02 22:37:29 +02:00
Raffson
66d741d0b3 Allow user to add navigation waypoints where possible 2023-08-27 21:11:10 +02:00
Raffson
819bd92d9a Re-add removed FF-interrupt choice causing error
Opening the options will trigger an exception which sort of breaks save-compatibility
2023-08-27 19:10:39 +02:00
M Chimiste
16b7ff2bb4 Create payloads folder in ME folder if needed (#200)
Added a check for the existence of a payloads folder.  Also made the _save_payloads lookup and path generation the same as _backup_payloads method to have consistency.
2023-08-27 19:10:00 +02:00
Raffson
19b9b8b0ea Update changelog.md 2023-08-26 20:45:11 +02:00
Raffson
f5cb293e44 Allow editing pretty_name for waypoints 2023-08-26 19:19:54 +02:00
Raffson
f22ad17651 Remove incompatible Escort task from L-39ZA 2023-08-26 11:13:40 +02:00
Raffson
9b9905c5a3 Tweaking helicopter stuff 2023-08-19 21:10:26 +02:00
Raffson
8a6b7b172c Fix #199
Adjust 'should_halt_sim' conditions for taxi & takeoff flight-states, fixing an infinite loop that would occur if the FF-interrupt setting would not match with the start-type of the flight
2023-08-19 15:14:23 +02:00
Raffson
4901443b7a Avoid zero-division exception 2023-08-16 19:08:04 +02:00
Raffson
789806637c Improve escort logic for helicopters
Babysteps #88
2023-08-15 21:50:20 +02:00
Raffson
270301958a Fix helicopters oscillating due to over-speeding 2023-08-14 20:25:35 +02:00
Raffson
1157d264ee Migrator update for SEAD-Sweep 2023-08-14 20:25:35 +02:00
Raffson
87399c9ada Update transit-network costs
Fix #169
2023-08-13 19:19:01 +02:00
Raffson
cc4db1deff Update changelog 2023-08-13 18:34:25 +02:00
Raffson
77b88de40e Migrator adjustment for 'radio_presets' 2023-08-13 18:31:57 +02:00
Raffson
7baba78bca Allow radio-presets to be defined in squadron files 2023-08-13 13:16:43 +02:00
Raffson
95f169bb58 Add HF-FM band to ARC-222 2023-08-13 13:16:42 +02:00
MetalStormGhost
7a9a8e1b4b Auto-ato AWACS & Tankers settings (#198)
* Auto-ato AWACS & Tankers settings

Split off the **Automatic AWACS package planning** and **Automatic Theater tanker package planning** settings from **Automatic package planning behavior** so players can choose to have AWACS and theater tankers auto-planned, while managing everything else themselves.

* Drop logic to child-classes

* Enable AWACS auto-planning by default

* Switch order of preconditions

---------

Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2023-08-12 19:09:19 +00:00
Raffson
046dccc4d5 Update contributors list 2023-08-12 19:25:36 +02:00
Raffson
15b0c26c5d Update version info for Su-57 mod 2023-08-12 19:15:14 +02:00
Raffson
7200c687af Update version info for Su-30 mod 2023-08-12 19:11:28 +02:00
MetalStormGhost
9b11eaa9bc Invasion of the Canary Islands campaign by NoGoodNews (#197)
* Invasion of the Canary Islands campaign version 2 by NoGoodNews

* Fine-tune description for Canary Islands campaign

* Bump Canary Islands version to 10.7

* OCD trippin'

---------

Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2023-08-12 18:45:20 +02:00
ingax01
d6504c3a09 updated SU-57 wepons (#196) 2023-08-11 07:40:04 +02:00
ingax01
15cb1839b0 updated the weapon classes (#194) 2023-08-10 07:13:39 +02:00
MetalStormGhost
633615c90d 1968 Yankee Station campaign version 2 by NoGoodNews
Changes to the original "1968 Yankee Station" are:  More FOBs with proper CTLD areas, denser (much denser) AAA and SAM coverage and better convoy roads. Also includes updated factions. All credit for this submission belongs to NoGoodNews.
2023-08-09 07:02:26 +02:00
MetalStormGhost
115bd8b22a Convert OPFOR untasked flights into Client slots
Added a new option in settings: Convert untasked OPFOR aircraft into client slots. This option will essentially convert the campaign into a sort of team vs. team engagement. There is still no way to plan the OPFOR missions, and there are no guarantees that there even will be any untasked aircraft available for players.

Split the Disable idle aircraft at airfields setting into Disable untasked BLUFOR aircraft at airfields and Disable untasked OPFOR aircraft at airfields.
2023-08-06 12:43:34 +02:00
MetalStormGhost
c51fa5c1df CAS engagement range
New option in Settings: CAS engagement range (nmi)
2023-08-02 19:22:15 +02:00
MetalStormGhost
a5104dd9ec USS Samuel Chase class fix
Corrected the class of the WWII asset pack ship USS Samuel Chase from Logistics (ground unit class) to LandingShip, in order to prevent it being spawned as part of AAA sites.
2023-08-02 19:18:31 +02:00
holyorangejuice
901c867d7f Battle4Georgia Update v1.2 (#189)
* WRL Faction Updates

Added aircraft to be more all inclusive.

* WRL Battle4Gerogia Overhaul

v1.2 changes to miz file and enemy units. Complete overhaul. Tested in WRL events.
2023-08-01 07:44:38 +02:00
holyorangejuice
5c06700873 WRL Faction Updates
Added aircraft to be more all inclusive.
2023-07-26 17:45:17 +02:00
Raffson
0bc645edeb Have DEAD flights fire all their HARMs first 2023-07-23 00:06:35 +02:00
Raffson
1e5cb94feb Fix mypy complaints 2023-07-23 00:06:35 +02:00
Raffson
53122ff149 Allow user-made custom injected weapons setup 2023-07-22 23:41:42 +02:00
Raffson
da75f41b01 Refactor payloads folder logic 2023-07-22 23:26:32 +02:00
Raffson
63e65744f1 Formatting -_- 2023-07-22 21:34:52 +02:00
Raffson
b8cce988f8 Fix wrong serialization when saving payload in new file 2023-07-22 21:33:45 +02:00
Raffson
85bbda4234 Notify user when save/backup was successful 2023-07-22 20:35:26 +02:00
Raffson
3f96334065 Correct changelog 2023-07-22 20:01:01 +02:00
Raffson
79ce31c920 Replace AIM-120Bs with C's for F-15E & F/A-18C 2023-07-22 20:01:00 +02:00
Raffson
0e5df2fd21 Get rid of all pydcs' PayloadDirectories usages 2023-07-22 20:01:00 +02:00
Turbolious
57319bd78b Update VSN_F100 payloads (#186)
Add loadout to conform to Retribution norms and add all missing loadouts.
All loadouts have been tested and work properly.
2023-07-22 19:20:19 +02:00
Turbolious
5bfab913fb Update F-4E.lua (#185)
Added missing CAS loadout
2023-07-22 00:04:16 +02:00
Raffson
0027c64e57 Add & Select newly saved payload to selector 2023-07-22 00:03:22 +02:00
Raffson
bc38563618 Update changelog 2023-07-21 23:35:47 +02:00
Raffson
493e69eb40 Use base_path instead 2023-07-21 22:51:13 +02:00
Raffson
15be9e3ce1 Fine-tune "save payload" logic 2023-07-21 22:21:45 +02:00
Raffson
64f6a10ced Attempt to fix index-out-of-range
Injecting pylons in the F-100 seems to go out of bounds here
2023-07-21 22:21:44 +02:00
Raffson
2cfd56c8da Add ability to save/backup payloads
Resolve #164
2023-07-21 21:47:25 +02:00
Raffson
60d11ffe2a Add comment wrt AGM-154B 2023-07-21 15:20:42 +02:00
Raffson
01f2d37fb8 Correct 2xAGM-154B's weapon data
Removing non-existing CLSID
2023-07-21 15:15:33 +02:00
Raffson
8e6464eb90 Add 2xAGM-154B 2023-07-21 15:15:04 +02:00
Raffson
2e6b98e9de Revert default payloads back to AGM-154A 2023-07-21 15:06:38 +02:00
Raffson
d095732a4e Bump version 2023-07-21 13:29:48 +02:00
Raffson
b83f3c41d7 Update changelog 2023-07-21 00:48:48 +02:00
Raffson
e497c7dc6c Another attempt to replace AGM-154A with AGM-154B 2023-07-21 00:46:13 +02:00
Raffson
1a52154b5a Custom weapons injection logic + A-4E AIM-9J injection 2023-07-21 00:03:38 +02:00
Raffson
6072b0181d Remove } from AGM-45B.yaml 2023-07-20 21:29:14 +02:00
Raffson
e3f9f611eb Tweak aircraft tasks & priorities 2023-07-20 20:50:51 +02:00
Turbolious
34f7b2c53d Missile fallback updates + add VSN F-4E payload (#179)
* Update AIM-7F.yaml

Fallback update for to the E2

* Update AIM-7F.yaml

* Aim-9 corrections

Year and fallback corrections
2023-07-20 19:20:41 +02:00
Turbolious
8472897e5f Update AIM-7 Aspide by VSN.yaml
Updated Fallback to AIM-7E-2
2023-07-20 19:17:45 +02:00
holyorangejuice
7c5f92b3c5 WRL Campaign Update (#184)
Minor changes to Damascus campaign. Redid the SAMs and yaml for it.
2023-07-20 19:16:24 +02:00
Turbolious
5d89ba769d Update AGM-45B.yaml
Found a missing CLSID for the AGM-45B
2023-07-20 08:13:00 +02:00
Turbolious
bdcbf81443 Remove double ‘year’ in AGM-45 (#181) 2023-07-20 08:11:52 +02:00
Turbolious
e5afa27b4a Update Aim-7F Fallback To Aim-7E-2 (#178) 2023-07-19 22:51:18 +02:00
Turbolious
51646639ff F-104 missing weapons
added the 2x Aim-9p/L variants and the Aspide MRAAM
2023-07-19 07:41:48 +02:00
Turbolious
428833ce38 Update AIM-9J-2X.yaml
Corrected capital letter error
2023-07-19 07:34:11 +02:00
Turbolious
4cb4058ded A2A weapons cleanup and additions. F-105D Payloads
Corrected Introduction years, added models, corrected CLSID issues and corrected fallback issues.

Added corrected and expanded F-105D customized Payloads.
2023-07-18 19:44:59 +02:00
Turbolious
20d35762a6 Update MiG-25RBT.lua + Add custom Alt History Factions (#174)
* Update MiG-25RBT.lua

Corrected naming conventions and capitalization.

* 1971 Alt History USA vs USSR Factions

My alt history factions for USA and USSR fighting in the Vietnam war

* Update VSN_F100.yaml

Remove SEAD Escort as it would require changes on client side
2023-07-18 00:11:10 +02:00
Turbolious
c0e8e240a1 F-100 Weapons and Roles update (#173)
Updated the VSN F-100 Payloads and added SEAD and SEAD Escort roles
2023-07-17 23:53:26 +02:00
Turbolious
850cb415c7 Update AGM-122A.yaml 2023-07-17 20:33:50 +02:00
Turbolious
3e42484be3 F104 Aim-9L 2x, AGM-122 Added
Added missing weapon class under the Aim-9L.
Added missing weapon AGM-122A SideARM
2023-07-17 20:33:50 +02:00
Raffson
adaf049eb9 Define fallback for AGM-88C 2023-07-16 23:01:29 +02:00
Raffson
bed1417c54 Add VSN F105 AIM-9P/L CLSIDs to weapons data 2023-07-16 23:01:05 +02:00
Raffson
98a10b59b9 Add SEAD payload to A-6A 2023-07-16 22:40:54 +02:00
Raffson
f5174281c7 Correct A-4's BARCAP payload 2023-07-16 22:11:13 +02:00
Raffson
7ec4073ee2 Update changelog 2023-07-16 21:59:14 +02:00
Raffson
80f81e3775 Remove OCA/Runway task from F-5E
Apparently the F-5E isn't capable of runway attack...
2023-07-16 21:38:28 +02:00
Raffson
d8c34a7057 Add payloads for MiG-25RBT
Co-Authored-By: Turbolious <39207917+Turbolious@users.noreply.github.com>
2023-07-16 21:23:57 +02:00
Raffson
595a5ac111 Fix flights not removed from map if auto-create cancelled 2023-07-16 21:19:04 +02:00
Raffson
024407e1e2 Initial POC for #165 2023-07-16 19:51:35 +02:00
Raffson
9d63bcbf69 Improve STRIKE where possible
- Plan number of aircraft more accurately
- Fine-tuning ingress waypoint tasks
- Add lineup waypoint for STRIKE flights, 10NM in front of ingress on the same heading as the target
2023-07-16 18:22:29 +02:00
Raffson
092ef88cab Revert "Use AGM-154B instead of AGM-154A"
This reverts commit 16b3d76684.

Unfortunately I forgot this still requires local changes to take effect...
2023-07-16 18:22:29 +02:00
Raffson
16b3d76684 Use AGM-154B instead of AGM-154A 2023-07-15 20:23:28 +02:00
dependabot[bot]
43b6cb8bb1 Bump tough-cookie from 4.1.2 to 4.1.3 in /client
Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/salesforce/tough-cookie/releases)
- [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md)
- [Commits](https://github.com/salesforce/tough-cookie/compare/v4.1.2...v4.1.3)

---
updated-dependencies:
- dependency-name: tough-cookie
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-15 18:05:42 +02:00
Raffson
009debd4e8 Follow-up on dependency updates 2023-07-15 17:55:55 +02:00
Raffson
6c87421c26 Cache liveries at startup 2023-07-15 17:46:31 +02:00
Raffson
21eff6cb87 Update changelog 2023-07-15 16:57:35 +02:00
Raffson
00bcec498c Update dependencies 2023-07-15 16:50:14 +02:00
Raffson
baac660f1d Update faction selection page when inverting map
Reported by Kerbo on discord
2023-07-15 16:32:13 +02:00
Raffson
63898ead87 Use group-attack for Anti-ship ingress 2023-07-15 13:44:58 +02:00
Christian Merrill
18f95acf87 Update Operation-Desert-Sabre.miz
Moved a problematic ammo depot
2023-07-12 07:16:07 +02:00
Raffson
cb136b8d9d Increase default number of pilots per squadron to 16
Resolves #162
2023-07-10 23:12:30 +02:00
Raffson
4ecc44da17 Preparations for v1.3.0 2023-07-10 23:12:30 +02:00
608 changed files with 33065 additions and 8544 deletions

View File

@@ -31,7 +31,7 @@ body:
If the bug was found in a test/development build, select "Test build" or "Development build"
and provide a link to the applicable build in the field below.
options:
- v1.1
- v1.2.1
- Test build
- Development build
- type: input

View File

@@ -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

@@ -13,6 +13,7 @@ jobs:
with:
src: "."
options: "--check"
version: "23.9.1"
mypy:
name: Type checking

View File

@@ -2,7 +2,7 @@
(Github Readme Banner and Splash screen Artwork by Andriy Dankovych, CC BY-SA 4.0)
[![Patreon](https://img.shields.io/badge/patreon-become%20a%20patron-orange?logo=patreon)](https://patreon.com/khopa)
[![Patreon](https://img.shields.io/badge/patreon-become%20a%20patron-orange?logo=patreon)](https://patreon.com/dcsliberation)
[![Download](https://img.shields.io/github/downloads/dcs-retribution/dcs-retribution/total?label=Download)](https://github.com/dcs-retribution/dcs-retribution/releases)

View File

@@ -1,3 +1,70 @@
# Retribution v1.3.0
#### Note: Re-save your missions in DCS' Mission Editor to avoid possible crashes due to datalink (usually the case when F-16C blk50s are used) when hosting missions on a dedicated server.
## Features/Improvements
* **[Engine]** Support for DCS v2.9.3.51704
* **[Package Planning]** Option to "Auto-Create" package
* **[Modding]** Custom weapons injection system (definition in aircraft's yaml file)
* **[Payload Editor]** Ability to save/back-up payloads
* **[Options]** New option in Settings: CAS engagement range (nmi)
* **[Options]** New option in Settings: Convert untasked OPFOR aircraft into client slots
* **[Options]** Split the **Disable idle aircraft at airfields** setting into **Disable untasked BLUFOR aircraft at airfields** and **Disable untasked OPFOR aircraft at airfields**
* **[Options]** Split off the **Automatic AWACS package planning** and **Automatic Theater tanker package planning** settings from **Automatic package planning behavior** so players can choose to have AWACS and theater tankers auto-planned, while managing everything else themselves
* **[Modding]** Updated support for Su-30 mod to V2.7.3 Beta
* **[Modding]** Updated support for Su-57 mod to build-04
* **[Modding]** Updated support for F-4B/C Phantom mod to 2.8.7.204
* **[Modding]** Updated Community A-4E-C mod version support to 2.2.0 release.
* **[Modding]** Added F/A-18E/F Super Hornet AI Tanker mod support (Chiller Juice Studios SuperBug Tanker AI version 1.4)
* **[Modding]** Added VSN Super Étendard mod support (v2.5.5)
* **[Modding]** Added F9F Panther mod support (version v2.8.7.101)
* **[Modding]** Updated Irondome support to IDF Assets Pack V1.1, adding support for the David's Sling
* **[Radios]** Added HF-FM band for AN/ARC-222
* **[Radios]** Ability to define preset channels for radios on squadron level (for human pilots only)
* **[Mission Planning]** Avoid helicopters being assigned as escort to planes and vice-versa
* **[Mission Planning]** Allow attack helicopters to escort other helicopters
* **[UI]** Allow changing waypoint names in FlightEdit's waypoints tab
* **[Waypoints]** Allow user to add navigation waypoints where possible without degrading to a custom flight-plan
* **[Campaign Management]** Improve squadron retreat logic to account for parking-slot sizes
* **[Autoplanner]** Support for auto-planning Air Assaults
* **[UI]** Improved frequency selector to support all modeled bands for every aircraft's intra-flight radio
* **[Options]** New options in Settings: Helicopter waypoint altitude (feet AGL) for combat & cruise waypoints
* **[Options]** New options in Settings: Spawn ground power trucks at ground starts in airbases/roadbases
* **[Options]** Option for hiding TGOs (with IADS roles) on MFD
* **[Plugins]** Splash Damage 2.1 with Clusters and Ship Radar effects.
* **[COMMs]** Aircraft-specific callsigns will now also be used.
* **[COMMs]** Ability to set a specific callsign to a flight.
* **[Mission Generator]** Channel terrain fix on exclusion zones, sea zones and inclusion zones
* **[Options]** Cheat-option for accessing Air Wing Config Dialog after campaign start (re-initializes turn if applied, thus plan your mission ___after___ making changes)
* **[Options]** Option to enable unlimited fuel for AI (player and non-player flights)
* **[Mission Generator]** F-15E Strike targets are automatically added as Mission Set 1
* **[Mission Generator]** Set F-14's IP waypoint according to the flight-plan's ingress point
* **[Mission Generator]** Automatically de-spawn aircraft when arrival/divert is an off-map spawn
* **[Options]** Option to de-spawn AI flights in the air if their start-type was manually set to In-Flight
* **[Config]** Preference setting to use custom Liberation payloads instead of prioritizing Retribution's default
* **[Config]** Preference setting to configure the server-port on which Retribution's back-end will run
* **[Options]** Made AI jettisoning empty fuel tanks optional (disabled by default)
* **[Options]** Add option (so it can be disabled when fixed in DCS) to force air-starts (except for the slots that work) at Nevatim due to https://forum.dcs.world/topic/335545-29-nevatim-ramp-starts-still-bugged/
* **[Cheat]** Add cheat option to manually manage REDFOR's TGOs
* **[UX]** Buy/Replace TGOs for free before the campaign has started
* **[Data]** Ability to define "cruise" & "combat" altitudes for airplanes
* **[Options]** Option to randomize altitudes for flights with airplanes
* **[Options]** Options to configure/override maximum mission distance for airplanes & helicopters
## Fixes
* **[Mission Generation]** Anti-ship strikes should use "group attack" in their attack-task
* **[New Game Wizard]** Faction selection overview doesn't update when inverting map
* **[New Game Wizard]** Aircraft mods are now handled better when they are disabled
* **[Payloads]** Added/Updated (missing) payloads
* **[Aircraft Tasking]** Revised aircraft tasking, filtering out incompatible tasks for several aircraft
* **[Data]** Corrected the class of the USS Samuel Chase from Logistics to LandingShip, in order to prevent it being spawned as part of AAA sites.
* **[Mission Generation]** Helicopters oscillating due to over-speeding
* **[Mission Generation]** Fix infinite loop when using "Fast-Forward to first contact"
* **[Capture Logic]** Release all parking slots when an airbase is captured
* **[Modding]** Swedish Military Assets Pack air defence presets are now correctly removed from the faction when the mod is disabled.
* **[Mission Generation]** Naval aircraft not always returning to carrier
* **[Mission Generation]** AI AirLift aircraft crashing into terrain due to insufficient waypoints
* **[Mission Generation]** Fix friendly AI shooting at fires on the front-line
# Retribution v1.2.1 (hotfix)
## Fixes
@@ -24,7 +91,7 @@
* AEW&C threat buffer distance (nmi)
* Theater tanker threat buffer distance (nmi)
* **[Options]** Improved the option to configure OPFOR autoplanner aggressiveness. The AI might now take even more risks and plan missions against defended targets.
* Added three new options in Settings:
* **[Options]** Added three new options in Settings:
* Autoplanner plans refueling flights for Strike packages
* Autoplanner plans refueling flights for OCA packages
* Autoplanner plans refueling flights for DEAD packages
@@ -176,10 +243,53 @@ BAI/ANTISHIP/DEAD/STRIKE/BARCAP/CAS/OCA/AIR-ASSAULT (main) missions
# Liberation:
## Features/Improvements
* **[Engine]** Support for DCS 2.8.6.41363, including F-15E support.
* **[Data]** Added support for the ARA Veinticinco de Mayo.
* **[Data]** Changed display name of the AI-only F-15E Strike Eagle for clarity.
* **[Flight Planning]** Improved IP selection for targets that are near the center of a threat zone.
* **[Flight Planning]** Moved CAS ingress point off the front line so that the AI begins their target search earlier.
* **[Flight Planning]** Loadouts and aircraft properties can now be set per-flight member. Warning: AI flights should not use mixed loadouts.
* **[Flight Planning]** Laser codes that are pre-assigned to weapons at mission start can now be chosen from a list in the loadout UI. This does not affect the aircraft's TGP, just the weapons. Currently only implemented for the F-15E S4+ and F-16C.
* **[Mission Generation]** Configured target and initial points for F-15E S4+.
* **[Modding]** Factions can now specify the ship type to be used for cargo shipping. The Handy Wind will be used by default, but WW2 factions can pick something more appropriate.
* **[Modding]** Unit variants can now set a display name separate from their ID.
* **[UI]** An error will be displayed when invalid fast-forward options are selected rather than beginning a never ending simulation.
* **[UI]** Added cheats for instantly repairing and destroying runways.
* **[UI]** Improved usability of the flight properties UI. It now shows human-readable names and uses more appropriate UI elements.
* **[UI]** The map now shows the real front line bounds.
## Fixes
* **[Campaign]** Fixed error when canceling squadron transfer if the current location would be exactly full.
* **[Data]** Fixed the class of the Samuel Chase so it can't be picked for a AAA or SHORAD site.
* **[Data]** Allow CH-47D, CH-53E and UH-60A to operate from carriers and LHAs.
* **[Data]** Added the F-15E's LANTIRN to the list of known targeting pods. Player F-15E flight with TGPs will now be assigned laser codes.
* **[Flight Planning]** Patrolling flight plans (CAS, CAP, refueling, etc) now handle TOT offsets.
* **[Mission Generation]** Restored previous AI behavior for anti-ship missions. A DCS update caused only a single aircraft in a flight to attack. The full flight will now attack like they used to.
* **[Mission Generation]** Fix generation of OCA Runway missions to allow LGBs to be used.
* **[Mission Generation]** Fixed AI flights flying far too slowly toward NAV points.
* **[Mission Generation]** Fixed "division by zero" error on mission generation when a flight has an "In-Flight" start type and starts on top of a mission waypoint.
* **[Modding]** Unit variants can now actually override base unit type properties.
* **[Plugins]** Fixed Lua errors in Skynet plugin that would occur whenever one coalition had no IADS nodes.
* **[UI]** Fixed deleting waypoints in custom flight plans deleting the wrong waypoint.
* **[UI]** Fixed flight properties UI to support F-15E S4+ laser codes.
* **[UI]** Fixed UI bug where altering an "ahead of package" TOT offset would change the offset back to a "behind pacakge" offset.
* **[UI]** Fixed bug where changing TOT offsets could result in flight startup times that are in the past.
* **[UI]** Flight plan paths are now drawn behind all other map elements, fixing rare cases where they could prevent other UI elements from being clickable.
# 8.1.0
Saves from 8.0.0 are compatible with 8.1.0
## Features/Improvements
* **[Engine]** Support for DCS 2.8.6.41363, including F-15E support.
* **[UI]** Flight loadout/properties tab is now scrollable.
## Fixes
* **[Campaign]** Fixed liveries for premade squadrons all being off-by-one.
* **[UI]** Fixed numbering of waypoints in the map and flight dialog (first waypoint is now 0 rather than 1).
# 8.0.0
Saves from 7.x are not compatible with 8.0.

1988
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"@types/react-redux": "^7.1.24",
"axios": "^1.1.2",
"axios": "^1.6.0",
"electron-window-state": "^5.0.3",
"esri-leaflet": "^3.0.8",
"leaflet": "^1.9.2",
@@ -62,11 +62,11 @@
},
"devDependencies": {
"@rtk-query/codegen-openapi": "^1.0.0",
"@trivago/prettier-plugin-sort-imports": "^3.3.0",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/leaflet": "^1.8.0",
"@types/redux-logger": "^3.0.9",
"@types/websocket": "^1.0.5",
"electron": "^21.1.0",
"electron": "^22.3.25",
"electron-is-dev": "^2.0.0",
"generate-license-file": "^2.0.0",
"identity-obj-proxy": "^3.0.0",

View File

@@ -50,14 +50,6 @@ const injectedRtkApi = api.injectEndpoints({
url: `/debug/waypoint-geometries/hold/${queryArg.flightId}`,
}),
}),
getDebugIpZones: build.query<
GetDebugIpZonesApiResponse,
GetDebugIpZonesApiArg
>({
query: (queryArg) => ({
url: `/debug/waypoint-geometries/ip/${queryArg.flightId}`,
}),
}),
getDebugJoinZones: build.query<
GetDebugJoinZonesApiResponse,
GetDebugJoinZonesApiArg
@@ -245,11 +237,6 @@ export type GetDebugHoldZonesApiResponse =
export type GetDebugHoldZonesApiArg = {
flightId: string;
};
export type GetDebugIpZonesApiResponse =
/** status 200 Successful Response */ IpZones;
export type GetDebugIpZonesApiArg = {
flightId: string;
};
export type GetDebugJoinZonesApiResponse =
/** status 200 Successful Response */ JoinZones;
export type GetDebugJoinZonesApiArg = {
@@ -379,12 +366,6 @@ export type HoldZones = {
permissibleZones: LatLng[][][];
preferredLines: LatLng[][];
};
export type IpZones = {
homeBubble: LatLng[][];
ipBubble: LatLng[][];
permissibleZone: LatLng[][];
safeZones: LatLng[][][];
};
export type JoinZones = {
homeBubble: LatLng[][];
targetBubble: LatLng[][];
@@ -426,6 +407,7 @@ export type Tgo = {
detection_ranges: number[];
dead: boolean;
sidc: string;
task?: string[];
};
export type SupplyRoute = {
id: string;
@@ -497,7 +479,6 @@ export const {
useSetControlPointDestinationMutation,
useClearControlPointDestinationMutation,
useGetDebugHoldZonesQuery,
useGetDebugIpZonesQuery,
useGetDebugJoinZonesQuery,
useListFlightsQuery,
useGetFlightByIdQuery,

View File

@@ -30,11 +30,6 @@ export const liberationApi = _liberationApi.enhanceEndpoints({
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
],
},
getDebugIpZones: {
providesTags: (result, error, arg) => [
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
],
},
getDebugJoinZones: {
providesTags: (result, error, arg) => [
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },

View File

@@ -57,6 +57,7 @@ describe("AirDefenseRangeLayer", () => {
detection_ranges: [20],
dead: false,
sidc: "",
task: [],
},
},
},
@@ -86,6 +87,7 @@ describe("AirDefenseRangeLayer", () => {
detection_ranges: [20],
dead: false,
sidc: "",
task: [],
},
},
},
@@ -125,6 +127,7 @@ describe("AirDefenseRangeLayer", () => {
detection_ranges: [20],
dead: false,
sidc: "",
task: [],
},
},
},

View File

@@ -1,7 +1,8 @@
import { Flight } from "../../api/liberationApi";
import { useGetCommitBoundaryForFlightQuery } from "../../api/liberationApi";
import WaypointMarker from "../waypointmarker";
import { ReactElement } from "react";
import { Polyline as LPolyline } from "leaflet";
import { ReactElement, useEffect, useRef } from "react";
import { Polyline } from "react-leaflet";
const BLUE_PATH = "#0084ff";
@@ -27,16 +28,44 @@ const pathColor = (props: FlightPlanProps) => {
function FlightPlanPath(props: FlightPlanProps) {
const color = pathColor(props);
const waypoints = props.flight.waypoints;
const polylineRef = useRef<LPolyline | null>(null);
// Flight paths should be drawn under everything else. There seems to be an
// issue where `interactive: false` doesn't do as its told (there's nuance,
// see the bug for details). It looks better if we draw the other elements on
// top of the flight plans anyway, so just push the flight plan to the back.
//
// https://github.com/dcs-liberation/dcs_liberation/issues/3295
//
// It's not possible to z-index a polyline (and leaflet says it never will be,
// because this is a limitation of SVG, not leaflet:
// https://github.com/Leaflet/Leaflet/issues/185), so we need to use
// bringToBack() to push the flight paths to the back of the drawing once
// they've been added to the map. They'll still draw on top of the map, but
// behind everything than was added before them. Anything added after always
// goes on top.
useEffect(() => {
if (props.selected) {
polylineRef.current?.bringToFront();
}
else {
polylineRef.current?.bringToBack();
}
});
if (waypoints == null) {
return <></>;
}
const points = waypoints
.filter((waypoint) => waypoint.include_in_path)
.map((waypoint) => waypoint.position);
return (
<Polyline
positions={points}
pathOptions={{ color: color, interactive: false }}
ref={polylineRef}
/>
);
}

View File

@@ -95,8 +95,12 @@ describe("FlightPlansLayer", () => {
},
},
});
expect(mockPolyline).toHaveBeenCalledTimes(2);
expect(mockLayerGroup).toBeCalledTimes(1);
// For some reason passing ref to PolyLine causes it and its group to be
// redrawn, so these numbers don't match what you'd expect from the test.
// It probably needs to be rewritten without mocks.
expect(mockPolyline).toHaveBeenCalledTimes(3);
expect(mockLayerGroup).toBeCalledTimes(2);
});
it("are not drawn if wrong coalition", () => {
renderWithProviders(<FlightPlansLayer blue={true} />, {

View File

@@ -53,6 +53,18 @@ export default function LiberationMap() {
<LayersControl.Overlay name="Air defenses" checked>
<TgosLayer categories={["aa"]} />
</LayersControl.Overlay>
<LayersControl.Overlay name="LORAD" >
<TgosLayer categories={["aa"]} task={"LORAD"} />
</LayersControl.Overlay>
<LayersControl.Overlay name="MERAD" >
<TgosLayer categories={["aa"]} task={"MERAD"} />
</LayersControl.Overlay>
<LayersControl.Overlay name="SHORAD" >
<TgosLayer categories={["aa"]} task={"SHORAD"} />
</LayersControl.Overlay>
<LayersControl.Overlay name="AAA" >
<TgosLayer categories={["aa"]} task={"AAA"} />
</LayersControl.Overlay>
<LayersControl.Overlay name="Factories" checked>
<TgosLayer categories={["factory"]} />
</LayersControl.Overlay>

View File

@@ -6,14 +6,21 @@ import { LayerGroup } from "react-leaflet";
interface TgosLayerProps {
categories?: string[];
exclude?: true;
task?: string;
}
export default function TgosLayer(props: TgosLayerProps) {
const allTgos = Object.values(useAppSelector(selectTgos).tgos);
const categoryFilter = props.categories ?? [];
const taskFilter = props.task ?? "";
const tgos = allTgos.filter(
(tgo) => categoryFilter.includes(tgo.category) === !(props.exclude ?? false)
);
(tgo) => {
if (taskFilter && tgo.task){
return taskFilter === tgo.task[0]
}
return categoryFilter.includes(tgo.category) === !(props.exclude ?? false)
}
);
return (
<LayerGroup>
{tgos.map((tgo) => {

View File

@@ -1,73 +0,0 @@
import { useGetDebugIpZonesQuery } from "../../api/liberationApi";
import { LayerGroup, Polygon } from "react-leaflet";
interface IpZonesProps {
flightId: string;
}
function IpZones(props: IpZonesProps) {
const { data, error, isLoading } = useGetDebugIpZonesQuery({
flightId: props.flightId,
});
if (isLoading) {
return <></>;
}
if (error) {
console.error("Error while loading waypoint IP zone info", error);
return <></>;
}
if (!data) {
console.log("Waypoint IP zone returned empty response");
return <></>;
}
return (
<>
<Polygon
positions={data.homeBubble}
color="#ffff00"
fillOpacity={0.1}
interactive={false}
/>
<Polygon
positions={data.ipBubble}
color="#bb89ff"
fillOpacity={0.1}
interactive={false}
/>
<Polygon
positions={data.permissibleZone}
color="#ffffff"
fillOpacity={0.1}
interactive={false}
/>
{data.safeZones.map((zone, idx) => {
return (
<Polygon
key={idx}
positions={zone}
color="#80BA80"
fillOpacity={0.1}
interactive={false}
/>
);
})}
</>
);
}
interface IpZonesLayerProps {
flightId: string | null;
}
export function IpZonesLayer(props: IpZonesLayerProps) {
return (
<LayerGroup>
{props.flightId ? <IpZones flightId={props.flightId} /> : <></>}
</LayerGroup>
);
}

View File

@@ -1,7 +1,6 @@
import { selectSelectedFlightId } from "../../api/flightsSlice";
import { useAppSelector } from "../../app/hooks";
import { HoldZonesLayer } from "./HoldZones";
import { IpZonesLayer } from "./IpZones";
import { JoinZonesLayer } from "./JoinZones";
import { LayersControl } from "react-leaflet";
@@ -16,9 +15,6 @@ export function WaypointDebugZonesControls() {
return (
<>
<LayersControl.Overlay name="IP zones">
<IpZonesLayer flightId={selectedFlightId} />
</LayersControl.Overlay>
<LayersControl.Overlay name="Join zones">
<JoinZonesLayer flightId={selectedFlightId} />
</LayersControl.Overlay>

View File

View File

@@ -191,11 +191,12 @@ class ForceGroup:
location: PresetLocation,
control_point: ControlPoint,
game: Game,
task: Optional[GroupTask],
) -> TheaterGroundObject:
"""Create a random TheaterGroundObject from the available templates"""
layout = random.choice(self.layouts)
return self.create_ground_object_for_layout(
layout, name, location, control_point, game
layout, name, location, control_point, game, task
)
def create_ground_object_for_layout(
@@ -205,9 +206,10 @@ class ForceGroup:
location: PresetLocation,
control_point: ControlPoint,
game: Game,
task: Optional[GroupTask],
) -> TheaterGroundObject:
"""Create a TheaterGroundObject for the given template"""
go = layout.create_ground_object(name, location, control_point)
go = layout.create_ground_object(name, location, control_point, task)
# Generate all groups using the randomization if it defined
for tgo_group in layout.groups:
for unit_group in tgo_group.unit_groups:
@@ -287,7 +289,7 @@ class ForceGroup:
unit.id = game.next_unit_id()
# Add unit name escaped so that we do not have scripting issues later
unit.name = escape_string_for_lua(
unit.unit_type.name if unit.unit_type else unit.type.name
unit.unit_type.variant_id if unit.unit_type else unit.type.name
)
unit.position = PointWithHeading.from_point(
ground_object.position + unit.position,

View File

@@ -1,4 +1,4 @@
from .flight import Flight
from .flighttype import FlightType
from .flight import Flight
from .flightwaypoint import FlightWaypoint
from .package import Package

View File

@@ -1,6 +1,8 @@
from __future__ import annotations
import random
import uuid
from collections.abc import Iterator
from datetime import datetime, timedelta
from typing import Any, List, Optional, TYPE_CHECKING
@@ -8,11 +10,15 @@ from dcs import Point
from dcs.planes import C_101CC, C_101EB, Su_33, FA_18C_hornet
from game.dcs.aircrafttype import AircraftType
from game.theater import ControlPoint, MissionTarget
from pydcs_extensions.hercules.hercules import Hercules
from .flightmembers import FlightMembers
from .flightroster import FlightRoster
from .flightstate import FlightState, Navigating, Uninitialized
from .flightstate.killed import Killed
from .loadouts import Loadout, Weapon
from .flighttype import FlightType
from .loadouts import Weapon
from ..radio.CallsignContainer import CallsignContainer
from ..radio.RadioFrequencyContainer import RadioFrequencyContainer
from ..radio.TacanContainer import TacanContainer
from ..radio.radios import RadioFrequency
@@ -29,10 +35,10 @@ if TYPE_CHECKING:
from game.sim.gameupdateevents import GameUpdateEvents
from game.sim.simulationresults import SimulationResults
from game.squadrons import Squadron, Pilot
from game.theater import ControlPoint
from game.transfers import TransferOrder
from game.data.weapons import WeaponType
from .flightmember import FlightMember
from .flightplans.flightplan import FlightPlan
from .flighttype import FlightType
from .flightwaypoint import FlightWaypoint
from .package import Package
from .starttype import StartType
@@ -40,7 +46,9 @@ if TYPE_CHECKING:
F18_TGP_PYLON: int = 4
class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
class Flight(
SidcDescribable, RadioFrequencyContainer, TacanContainer, CallsignContainer
):
def __init__(
self,
package: Package,
@@ -54,37 +62,33 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
roster: Optional[FlightRoster] = None,
frequency: Optional[RadioFrequency] = None,
channel: Optional[TacanChannel] = None,
callsign: Optional[str] = None,
callsign_tcn: Optional[str] = None,
claim_inv: bool = True,
) -> None:
self.id = uuid.uuid4()
self.package = package
self.coalition = squadron.coalition
self.squadron = squadron
self.flight_type = flight_type
if claim_inv:
self.squadron.claim_inventory(count)
if roster is None:
self.roster = FlightRoster(self.squadron, initial_size=count)
self.roster = FlightMembers(self, initial_size=count)
else:
self.roster = roster
self.roster = FlightMembers.from_roster(self, roster)
self.divert = divert
self.flight_type = flight_type
self.loadout = Loadout.default_for(self)
if self.squadron.aircraft.name == "F-15I Ra'am":
self.loadout.pylons[16] = Weapon.with_clsid(
"{IDF_MODS_PROJECT_F-15I_Raam_Dome}"
)
self.start_type = start_type
self.use_custom_loadout = False
self.custom_name = custom_name
self.group_id: int = 0
self.frequency = frequency
if self.unit_type.dcs_unit_type.tacan:
self.tacan = channel
self.tcn_name = callsign
self.tcn_name = callsign_tcn
self.initialize_fuel()
self.use_same_loadout_for_all_members = True
# Only used by transport missions.
self.cargo = cargo
@@ -111,11 +115,35 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
is_f18 = self.squadron.aircraft.dcs_unit_type.id == FA_18C_hornet.id
on_land = not self.squadron.location.is_fleet
if on_land and is_f18 and self.coalition.game.settings.atflir_autoswap:
self.loadout.pylons[F18_TGP_PYLON] = Weapon.with_clsid(
str(
FA_18C_hornet.Pylon4.AN_AAQ_28_LITENING___Targeting_Pod_[1]["clsid"]
for fm in self.roster.members:
fm.loadout.pylons[F18_TGP_PYLON] = Weapon.with_clsid(
str(
FA_18C_hornet.Pylon4.AN_AAQ_28_LITENING___Targeting_Pod[1][
"clsid"
]
)
)
)
# altitude offset for planes
offset_factor = self.coalition.game.settings.max_plane_altitude_offset
offset_factor = random.randint(0, offset_factor)
self.plane_altitude_offset = 1000 * offset_factor * random.choice([-1, 1])
@property
def available_callsigns(self) -> List[str]:
callsigns = set()
dcs_unit = self.squadron.aircraft.dcs_unit_type
category = dcs_unit.category
category = "Air" if category == "Interceptor" else category
for name in self.squadron.coalition.faction.country.callsign[category]:
callsigns.add(name)
if hasattr(dcs_unit, "callnames"):
country_name = self.squadron.coalition.faction.country.name
for c in dcs_unit.callnames:
if "Combined Joint Task Forces" in country_name or c == country_name:
for name in dcs_unit.callnames[c]:
callsigns.add(name)
return sorted(callsigns)
@property
def flight_plan(self) -> FlightPlan[Any]:
@@ -138,7 +166,11 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
def __setstate__(self, state: dict[str, Any]) -> None:
state["state"] = Uninitialized(self, state["squadron"].settings)
if "use_same_loadout_for_all_members" not in state:
state["use_same_loadout_for_all_members"] = True
self.__dict__.update(state)
if isinstance(self.roster, FlightRoster):
self.roster = FlightMembers.from_roster(self, self.roster)
@property
def blue(self) -> bool:
@@ -188,14 +220,17 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
def is_hercules(self) -> bool:
return self.unit_type == AircraftType.named("C-130J-30 Super Hercules")
@property
def from_cp(self) -> ControlPoint:
return self.departure
@property
def points(self) -> List[FlightWaypoint]:
return self.flight_plan.waypoints[1:]
@property
def custom_targets(self) -> List[MissionTarget]:
return [
MissionTarget(wpt.name, wpt.position)
for wpt in self.flight_plan.layout.custom_waypoints
]
def position(self) -> Point:
return self.state.estimate_position()
@@ -210,6 +245,9 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
def missing_pilots(self) -> int:
return self.roster.missing_pilots
def iter_members(self) -> Iterator[FlightMember]:
yield from self.roster.members
def set_flight_type(self, var: FlightType) -> None:
self.flight_type = var
@@ -239,6 +277,11 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
elif self.departure.cptype.name in ["FARP", "FOB"] and not self.is_helo:
self.fuel = unit_type.fuel_max * 0.75
def any_member_has_weapon_of_type(self, weapon_type: WeaponType) -> bool:
return any(
m.loadout.has_weapon_of_type(weapon_type) for m in self.iter_members()
)
def __repr__(self) -> str:
return self.__str__()
@@ -298,12 +341,12 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
Killed(self.state.estimate_position(), self, self.squadron.settings)
)
events.update_flight(self)
for pilot in self.roster.pilots:
for pilot in self.roster.iter_pilots():
if pilot is not None:
results.kill_pilot(self, pilot)
def recreate_flight_plan(self) -> None:
self._flight_plan_builder.regenerate()
def recreate_flight_plan(self, dump_debug_info: bool = False) -> None:
self._flight_plan_builder.regenerate(dump_debug_info)
@staticmethod
def clone_flight(flight: Flight) -> Flight:

49
game/ato/flightmember.py Normal file
View File

@@ -0,0 +1,49 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from game.ato.loadouts import Loadout
from game.lasercodes import LaserCode
if TYPE_CHECKING:
from game.squadrons import Pilot
class FlightMember:
def __init__(self, pilot: Pilot | None, loadout: Loadout) -> None:
self.pilot = pilot
self.loadout = loadout
self.use_custom_loadout = False
self.tgp_laser_code: LaserCode | None = None
self.weapon_laser_code: LaserCode | None = None
self.properties: dict[str, bool | float | int] = {}
def __setstate__(self, state: dict[str, Any]) -> None:
if "tgp_laser_code" not in state:
state["tgp_laser_code"] = None
if "weapon_laser_code" not in state:
state["weapon_laser_code"] = None
self.__dict__.update(state)
def assign_tgp_laser_code(self, code: LaserCode) -> None:
if self.tgp_laser_code is not None:
raise RuntimeError(
f"{self.pilot} already has already been assigned laser code "
f"{self.tgp_laser_code}"
)
self.tgp_laser_code = code
def release_tgp_laser_code(self) -> None:
if self.tgp_laser_code is None:
raise RuntimeError(f"{self.pilot} has no assigned laser code")
if self.weapon_laser_code == self.tgp_laser_code:
self.weapon_laser_code = None
self.tgp_laser_code.release()
self.tgp_laser_code = None
@property
def is_player(self) -> bool:
if self.pilot is None:
return False
return self.pilot.player

100
game/ato/flightmembers.py Normal file
View File

@@ -0,0 +1,100 @@
from __future__ import annotations
from collections.abc import Iterator
from typing import Optional, TYPE_CHECKING
from .flightmember import FlightMember
from .flightroster import FlightRoster
from .iflightroster import IFlightRoster
from .loadouts import Loadout
from ..data.weapons import Weapon
if TYPE_CHECKING:
from game.squadrons import Pilot, Squadron
from .flight import Flight
class FlightMembers(IFlightRoster):
def __init__(self, flight: Flight, initial_size: int = 0) -> None:
self.flight = flight
self.members: list[FlightMember] = []
self.resize(initial_size)
@property
def squadron(self) -> Squadron:
return self.flight.squadron
@staticmethod
def from_roster(flight: Flight, roster: FlightRoster) -> FlightMembers:
members = FlightMembers(flight)
loadout = Loadout.default_for(flight)
if flight.squadron.aircraft.variant_id == "F-15I Ra'am":
loadout.pylons[16] = Weapon.with_clsid("{IDF_MODS_PROJECT_F-15I_Raam_Dome}")
members.members = [FlightMember(p, loadout) for p in roster.pilots]
return members
def iter_pilots(self) -> Iterator[Pilot | None]:
yield from (m.pilot for m in self.members)
def pilot_at(self, idx: int) -> Pilot | None:
return self.members[idx].pilot
@property
def max_size(self) -> int:
return len(self.members)
@property
def player_count(self) -> int:
return len([m for m in self.members if m.is_player])
@property
def missing_pilots(self) -> int:
return len([m for m in self.members if m.pilot is None])
def resize(self, new_size: int) -> None:
if self.max_size > new_size:
for member in self.members[new_size:]:
if (pilot := member.pilot) is not None:
self.flight.squadron.return_pilot(pilot)
if (code := member.tgp_laser_code) is not None:
code.release()
self.members = self.members[:new_size]
return
if self.max_size:
loadout = self.members[0].loadout.clone()
else:
loadout = Loadout.default_for(self.flight)
if self.flight.squadron.aircraft.variant_id == "F-15I Ra'am":
loadout.pylons[16] = Weapon.with_clsid("{IDF_MODS_PROJECT_F-15I_Raam_Dome}")
for _ in range(new_size - self.max_size):
member = FlightMember(self.flight.squadron.claim_available_pilot(), loadout)
member.use_custom_loadout = loadout.is_custom
self.members.append(member)
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
if pilot is not None:
self.flight.squadron.claim_pilot(pilot)
if (current_pilot := self.pilot_at(index)) is not None:
self.flight.squadron.return_pilot(current_pilot)
self.members[index].pilot = pilot
def clear(self) -> None:
self.flight.squadron.return_pilots(
[p for p in self.iter_pilots() if p is not None]
)
for member in self.members:
if (code := member.tgp_laser_code) is not None:
code.release()
def use_same_loadout_for_all_members(self) -> None:
if not self.members:
return
loadout = self.members[0].loadout
for member in self.members[1:]:
# Do not clone the loadout, we want any changes in the UI to be mirrored
# across all flight members.
member.loadout = loadout
def use_distinct_loadouts_for_each_member(self) -> None:
for member in self.members:
member.loadout = member.loadout.clone()

View File

@@ -6,7 +6,7 @@ from typing import Type
from game.ato.flightplans.ibuilder import IBuilder
from game.ato.flightplans.patrolling import PatrollingFlightPlan, PatrollingLayout
from game.ato.flightplans.waypointbuilder import WaypointBuilder
from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles
from game.utils import Distance, Heading, Speed, knots, meters, nautical_miles
class AewcFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
@@ -49,7 +49,7 @@ class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]):
# Station 80nm outside the threat zone.
threat_buffer = nautical_miles(
self.flight.coalition.game.settings.aewc_threat_buffer_min_distance
self.coalition.game.settings.aewc_threat_buffer_min_distance
)
if self.threat_zones.threatened(location.position):
orbit_distance = distance_to_threat + threat_buffer
@@ -68,12 +68,9 @@ class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]):
orbit_heading.left.degrees, racetrack_half_distance
)
builder = WaypointBuilder(self.flight, self.coalition)
builder = WaypointBuilder(self.flight)
if self.flight.unit_type.patrol_altitude is not None:
altitude = self.flight.unit_type.patrol_altitude
else:
altitude = feet(25000)
altitude = builder.get_patrol_altitude
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
@@ -90,7 +87,8 @@ class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]):
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
custom_waypoints=list(),
)
def build(self) -> AewcFlightPlan:
def build(self, dump_debug_info: bool = False) -> AewcFlightPlan:
return AewcFlightPlan(self.flight, self.layout())

View File

@@ -1,15 +1,19 @@
from __future__ import annotations
import random
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
from game.theater.controlpoint import ControlPointType
from game.theater.missiontarget import MissionTarget
from game.utils import Distance, feet, meters
from ._common_ctld import generate_random_ctld_point
from .ibuilder import IBuilder
from .formationattack import (
FormationAttackLayout,
FormationAttackBuilder,
FormationAttackFlightPlan,
)
from .planningerror import PlanningError
from .uizonedisplay import UiZone, UiZoneDisplay
from .waypointbuilder import WaypointBuilder
@@ -22,51 +26,63 @@ if TYPE_CHECKING:
@dataclass
class AirAssaultLayout(StandardLayout):
class AirAssaultLayout(FormationAttackLayout):
# The pickup point is optional because we don't always need to load the cargo. When
# departing from a carrier, LHA, or off-map spawn, the cargo is pre-loaded.
pickup: FlightWaypoint | None
nav_to_ingress: list[FlightWaypoint]
ingress: FlightWaypoint
drop_off: FlightWaypoint | None
pickup: FlightWaypoint | None = None
drop_off: FlightWaypoint | None = None
# This is an implementation detail used by CTLD. The aircraft will not go to this
# waypoint. It is used by CTLD as the destination for unloaded troops.
target: FlightWaypoint
nav_to_home: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
if self.pickup is not None:
yield self.pickup
yield from self.nav_to_ingress
yield from self.nav_to
yield self.join
yield self.ingress
if self.drop_off is not None:
yield self.drop_off
yield self.target
yield from self.nav_to_home
yield self.targets[0]
yield from self.nav_from
yield self.arrival
if self.divert is not None:
yield self.divert
yield self.bullseye
yield from self.custom_waypoints
class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@property
def is_airassault(self) -> bool:
return True
@property
def tot_waypoint(self) -> FlightWaypoint:
if self.flight.is_helo and self.layout.drop_off is not None:
return self.layout.drop_off
return self.layout.target
return self.layout.targets[0]
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.tot_waypoint:
@property
def ingress_time(self) -> datetime:
tot = self.tot
travel_time = self.travel_time_between_waypoints(
self.layout.ingress, self.tot_waypoint
)
return tot - travel_time
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint is self.tot_waypoint:
return self.tot
elif waypoint is self.layout.ingress:
return self.ingress_time
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
@@ -74,17 +90,21 @@ 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:
return UiZone(
[self.layout.target.position],
[self.layout.targets[0].position],
self.ctld_target_zone_radius,
)
class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
def layout(self) -> AirAssaultLayout:
if not self.flight.is_helo and not self.flight.is_hercules:
raise PlanningError(
@@ -92,10 +112,10 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
)
assert self.package.waypoints is not None
altitude = feet(1500) if self.flight.is_helo else self.doctrine.ingress_altitude
altitude_is_agl = self.flight.is_helo
builder = WaypointBuilder(self.flight)
builder = WaypointBuilder(self.flight, self.coalition)
altitude = builder.get_cruise_altitude
altitude_is_agl = self.flight.is_helo
if self.flight.is_hercules or self.flight.departure.cptype in [
ControlPointType.AIRCRAFT_CARRIER_GROUP,
@@ -113,37 +133,46 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
self._generate_ctld_pickup(),
)
)
pickup.alt = altitude
pickup_position = pickup.position
ingress = builder.ingress(
FlightWaypointType.INGRESS_AIR_ASSAULT,
self.package.waypoints.ingress,
self.package.target,
)
assault_area = builder.assault_area(self.package.target)
heading = self.package.target.position.heading_between_point(pickup_position)
if self.flight.is_hercules:
assault_area.only_for_player = False
assault_area.alt = feet(1000)
# TODO: define CTLD dropoff zones in campaign miz?
drop_off_zone = MissionTarget(
"Dropoff zone",
self.package.target.position.point_from_heading(heading, 1200),
)
tgt = self.package.target
if isinstance(tgt, CTLD) and tgt.ctld_zones:
top3 = sorted(
tgt.ctld_zones, key=lambda x: ingress.position.distance_to_point(x[0])
)[:3]
pos, dist = random.choice(top3)
drop_pos = pos.random_point_within(dist)
else:
heading = tgt.position.heading_between_point(ingress.position)
drop_pos = tgt.position.point_from_heading(heading, 1200)
drop_off_zone = MissionTarget("Dropoff zone", drop_pos)
dz = builder.dropoff_zone(drop_off_zone) if self.flight.is_helo else None
return AirAssaultLayout(
departure=builder.takeoff(self.flight.departure),
pickup=pickup,
nav_to_ingress=builder.nav_path(
nav_to=builder.nav_path(
pickup_position,
self.package.waypoints.ingress,
altitude,
altitude_is_agl,
),
ingress=builder.ingress(
FlightWaypointType.INGRESS_AIR_ASSAULT,
self.package.waypoints.ingress,
self.package.target,
),
ingress=ingress,
drop_off=dz,
target=assault_area,
nav_to_home=builder.nav_path(
targets=[assault_area],
nav_from=builder.nav_path(
drop_off_zone.position,
self.flight.arrival.position,
altitude,
@@ -152,9 +181,14 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
hold=None,
join=builder.join(ingress.position),
split=builder.split(self.flight.arrival.position),
refuel=None,
custom_waypoints=list(),
)
def build(self) -> AirAssaultFlightPlan:
def build(self, dump_debug_info: bool = False) -> AirAssaultFlightPlan:
return AirAssaultFlightPlan(self.flight, self.layout())
def _generate_ctld_pickup(self) -> Point:

View File

@@ -1,17 +1,20 @@
from __future__ import annotations
import random
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from datetime import datetime
from typing import Optional
from typing import TYPE_CHECKING, Type
from game.theater.missiontarget import MissionTarget
from game.utils import feet
from game.utils import Distance
from ._common_ctld import generate_random_ctld_point
from .ibuilder import IBuilder
from .planningerror import PlanningError
from .standard import StandardFlightPlan, StandardLayout
from .waypointbuilder import WaypointBuilder
from ..flightwaypointtype import FlightWaypointType
from ...theater.interfaces.CTLD import CTLD
if TYPE_CHECKING:
@@ -21,14 +24,17 @@ if TYPE_CHECKING:
@dataclass
class AirliftLayout(StandardLayout):
nav_to_pickup: list[FlightWaypoint]
pickup_ascent: FlightWaypoint | None
pickup_descent: FlightWaypoint | None
# There will not be a pickup waypoint when the pickup airfield is the departure
# airfield for cargo planes, as the cargo is pre-loaded. Helicopters will still pick
# up the cargo near the airfield.
pickup: FlightWaypoint | None
# pickup_zone will be used for player flights to create the CTLD stuff
ctld_pickup_zone: FlightWaypoint | None
drop_off_ascent: FlightWaypoint | None
nav_to_drop_off: list[FlightWaypoint]
drop_off_descent: FlightWaypoint | None
# There will not be a drop-off waypoint when the drop-off airfield and the arrival
# airfield is the same for a cargo plane, as planes will land to unload and we don't
# want a double landing. Helicopters will still drop their cargo near the airfield
@@ -36,39 +42,57 @@ class AirliftLayout(StandardLayout):
drop_off: FlightWaypoint | None
# drop_off_zone will be used for player flights to create the CTLD stuff
ctld_drop_off_zone: FlightWaypoint | None
nav_to_home: list[FlightWaypoint]
return_ascent: FlightWaypoint
return_descent: FlightWaypoint
def add_waypoint(
self, wpt: FlightWaypoint, next_wpt: Optional[FlightWaypoint]
) -> bool:
new_wpt = self.get_midpoint(wpt, next_wpt)
if wpt.waypoint_type in [
FlightWaypointType.PICKUP_ZONE,
FlightWaypointType.CARGO_STOP,
]:
self.nav_to_drop_off.insert(0, new_wpt)
return True
return super().add_waypoint(wpt, next_wpt)
def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
if super().delete_waypoint(waypoint):
return True
if waypoint in self.nav_to_pickup:
self.nav_to_pickup.remove(waypoint)
return True
elif waypoint in self.nav_to_drop_off:
if waypoint in self.nav_to_drop_off:
self.nav_to_drop_off.remove(waypoint)
return True
elif waypoint in self.nav_to_home:
self.nav_to_home.remove(waypoint)
elif super().delete_waypoint(waypoint):
return True
return False
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.nav_to_pickup
if self.pickup_ascent is not None:
yield self.pickup_ascent
yield from self.nav_to
if self.pickup_descent is not None:
yield self.pickup_descent
if self.pickup is not None:
yield self.pickup
if self.ctld_pickup_zone is not None:
yield self.ctld_pickup_zone
if self.drop_off_ascent is not None:
yield self.drop_off_ascent
yield from self.nav_to_drop_off
if self.drop_off_descent is not None:
yield self.drop_off_descent
if self.drop_off is not None:
yield self.drop_off
yield self.return_ascent
if self.ctld_drop_off_zone is not None:
yield self.ctld_drop_off_zone
yield from self.nav_to_home
yield from self.nav_from
yield self.return_descent
yield self.arrival
if self.divert is not None:
yield self.divert
yield self.bullseye
yield from self.custom_waypoints
class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
@@ -84,16 +108,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
@@ -105,20 +133,52 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
"Cannot plan transport mission for flight with no cargo."
)
altitude = feet(1500)
altitude_is_agl = True
builder = WaypointBuilder(self.flight)
builder = WaypointBuilder(self.flight, self.coalition)
altitude = builder.get_cruise_altitude
altitude_is_agl = self.flight.is_helo
pickup_ascent = None
pickup_descent = None
pickup = None
drop_off_ascent = None
drop_off_descent = None
drop_off = None
pickup_zone = None
drop_off_zone = None
if cargo.origin != self.flight.departure:
pickup = builder.cargo_stop(cargo.origin)
pickup_ascent = self._create_ascent_or_descent(
builder,
self.flight.departure.position,
cargo.origin.position,
altitude,
altitude_is_agl,
)
pickup_descent = self._create_ascent_or_descent(
builder,
cargo.origin.position,
self.flight.departure.position,
altitude,
altitude_is_agl,
)
if cargo.next_stop != self.flight.arrival:
drop_off = builder.cargo_stop(cargo.next_stop)
drop_off_ascent = self._create_ascent_or_descent(
builder,
cargo.origin.position,
cargo.next_stop.position,
altitude,
altitude_is_agl,
)
drop_off_descent = self._create_ascent_or_descent(
builder,
cargo.next_stop.position,
cargo.origin.position,
altitude,
altitude_is_agl,
)
if self.flight.is_helo:
# Create CTLD Zones for Helo flights
@@ -139,31 +199,57 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
altitude_is_agl,
)
return_ascent = self._create_ascent_or_descent(
builder,
cargo.next_stop.position
if cargo.next_stop != self.flight.arrival
else cargo.origin.position,
self.flight.arrival.position,
altitude,
altitude_is_agl,
)
return_descent = self._create_ascent_or_descent(
builder,
self.flight.arrival.position,
cargo.next_stop.position
if cargo.next_stop != self.flight.arrival
else cargo.origin.position,
altitude,
altitude_is_agl,
)
return AirliftLayout(
departure=builder.takeoff(self.flight.departure),
nav_to_pickup=nav_to_pickup,
pickup_ascent=pickup_ascent,
nav_to=nav_to_pickup,
pickup_descent=pickup_descent,
pickup=pickup,
ctld_pickup_zone=pickup_zone,
drop_off_ascent=drop_off_ascent,
nav_to_drop_off=builder.nav_path(
cargo.origin.position,
cargo.next_stop.position,
altitude,
altitude_is_agl,
),
drop_off_descent=drop_off_descent,
drop_off=drop_off,
ctld_drop_off_zone=drop_off_zone,
nav_to_home=builder.nav_path(
return_ascent=return_ascent,
nav_from=builder.nav_path(
cargo.origin.position,
self.flight.arrival.position,
altitude,
altitude_is_agl,
),
return_descent=return_descent,
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
custom_waypoints=list(),
)
def build(self) -> AirliftFlightPlan:
def build(self, dump_debug_info: bool = False) -> AirliftFlightPlan:
return AirliftFlightPlan(self.flight, self.layout())
def _generate_ctld_pickup(self) -> Point:
@@ -177,3 +263,18 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
if cargo and cargo.transport and isinstance(cargo.transport.destination, CTLD):
return generate_random_ctld_point(cargo.transport.destination)
raise RuntimeError("Could not generate CTLD dropoff")
@staticmethod
def _create_ascent_or_descent(
builder: WaypointBuilder,
start: Point,
end: Point,
alt: Distance,
agl: bool,
) -> FlightWaypoint:
distance = start.distance_to_point(end)
rdistance = 1000 if agl else min(distance / 10, 20000)
heading = round(start.heading_between_point(end))
rheading = random.randint(heading - 30, heading + 30) % 360
pos = start.point_from_heading(float(rheading), rdistance)
return builder.nav(pos, alt, agl)

View File

@@ -41,5 +41,5 @@ class Builder(FormationAttackBuilder[AntiShipFlightPlan, FormationAttackLayout])
def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> list[StrikeTarget]:
return [StrikeTarget(f"{g.group_name} at {tgo.name}", g) for g in tgo.groups]
def build(self) -> AntiShipFlightPlan:
def build(self, dump_debug_info: bool = False) -> AntiShipFlightPlan:
return AntiShipFlightPlan(self.flight, self.layout())

View File

@@ -39,5 +39,5 @@ class Builder(FormationAttackBuilder[BaiFlightPlan, FormationAttackLayout]):
return self._build(FlightWaypointType.INGRESS_BAI, targets)
def build(self) -> BaiFlightPlan:
def build(self, dump_debug_info: bool = False) -> BaiFlightPlan:
return BaiFlightPlan(self.flight, self.layout())

View File

@@ -1,11 +1,10 @@
from __future__ import annotations
import random
from datetime import timedelta
from typing import Type
from game.theater import FrontLine
from game.utils import Distance, Speed, feet
from game.utils import Distance, Speed
from .capbuilder import CapBuilder
from .invalidobjectivelocation import InvalidObjectiveLocation
from .patrolling import PatrollingFlightPlan, PatrollingLayout
@@ -41,14 +40,9 @@ class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
start_pos, end_pos = self.cap_racetrack_for_objective(location, barcap=True)
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
patrol_alt = max(
self.doctrine.min_patrol_altitude,
min(self.doctrine.max_patrol_altitude, randomized_alt),
)
builder = WaypointBuilder(self.flight)
patrol_alt = builder.get_patrol_altitude
builder = WaypointBuilder(self.flight, self.coalition)
start, end = builder.race_track(start_pos, end_pos, patrol_alt)
return PatrollingLayout(
@@ -64,7 +58,8 @@ class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
custom_waypoints=list(),
)
def build(self) -> BarCapFlightPlan:
def build(self, dump_debug_info: bool = False) -> BarCapFlightPlan:
return BarCapFlightPlan(self.flight, self.layout())

View File

@@ -6,13 +6,16 @@ from datetime import timedelta
from typing import TYPE_CHECKING, Type
from game.theater import FrontLine
from game.utils import Distance, Speed, kph, meters
from game.utils import Distance, Speed, kph, dcs_to_shapely_point
from game.utils import nautical_miles
from .ibuilder import IBuilder
from .invalidobjectivelocation import InvalidObjectiveLocation
from .patrolling import PatrollingFlightPlan, PatrollingLayout
from .uizonedisplay import UiZone, UiZoneDisplay
from .waypointbuilder import WaypointBuilder
from ..flightwaypointtype import FlightWaypointType
from ...flightplan.ipsolver import IpSolver
from ...persistency import waypoint_debug_directory
if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint
@@ -20,19 +23,21 @@ if TYPE_CHECKING:
@dataclass
class CasLayout(PatrollingLayout):
target: FlightWaypoint
ingress: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.nav_to
if self.ingress:
yield self.ingress
yield self.patrol_start
yield self.target
yield self.patrol_end
yield from self.nav_from
yield self.arrival
if self.divert is not None:
yield self.divert
yield self.bullseye
yield from self.custom_waypoints
class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay):
@@ -53,12 +58,13 @@ class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay):
@property
def engagement_distance(self) -> Distance:
max_length = self.flight.coalition.game.settings.max_frontline_width * 1000
return meters(max_length) / 2
return nautical_miles(
self.flight.coalition.game.settings.cas_engagement_range_distance
)
@property
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
return {self.layout.patrol_start, self.layout.target, self.layout.patrol_end}
return {self.layout.ingress, self.layout.patrol_start, self.layout.patrol_end}
def request_escort_at(self) -> FlightWaypoint | None:
return self.layout.patrol_start
@@ -67,14 +73,17 @@ class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay):
return self.layout.patrol_end
def ui_zone(self) -> UiZone:
midpoint = (
self.layout.patrol_start.position + self.layout.patrol_end.position
) / 2
return UiZone(
[self.layout.target.position],
[midpoint],
self.engagement_distance,
)
class Builder(IBuilder[CasFlightPlan, CasLayout]):
def layout(self) -> CasLayout:
def layout(self, dump_debug_info: bool) -> CasLayout:
location = self.package.target
if not isinstance(location, FrontLine):
@@ -84,49 +93,79 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]):
FrontLineConflictDescription,
)
bounds = FrontLineConflictDescription.frontline_bounds(
location, self.theater, self.coalition.game.settings
)
ingress = bounds.left_position
center = bounds.center
egress = bounds.right_position
bounds = FrontLineConflictDescription.frontline_bounds(location, self.theater)
patrol_start = bounds.left_position
patrol_end = bounds.right_position
ingress_distance = ingress.distance_to_point(self.flight.departure.position)
egress_distance = egress.distance_to_point(self.flight.departure.position)
if egress_distance < ingress_distance:
ingress, egress = egress, ingress
start_distance = patrol_start.distance_to_point(self.flight.departure.position)
end_distance = patrol_end.distance_to_point(self.flight.departure.position)
if end_distance < start_distance:
patrol_start, patrol_end = patrol_end, patrol_start
builder = WaypointBuilder(self.flight, self.coalition)
builder = WaypointBuilder(self.flight)
is_helo = self.flight.unit_type.dcs_unit_type.helicopter
ingress_egress_altitude = (
self.doctrine.ingress_altitude if not is_helo else meters(50)
ingress_egress_altitude = builder.get_combat_altitude
use_agl_patrol_altitude = is_helo
ip_solver = IpSolver(
dcs_to_shapely_point(self.flight.departure.position),
dcs_to_shapely_point(patrol_start),
self.doctrine,
self.threat_zones.all,
)
use_agl_ingress_egress = is_helo
ip_solver.set_debug_properties(
waypoint_debug_directory() / "IP", self.theater.terrain
)
ingress_point_shapely = ip_solver.solve()
if dump_debug_info:
ip_solver.dump_debug_info()
ingress_point = patrol_start.new_in_same_map(
ingress_point_shapely.x, ingress_point_shapely.y
)
patrol_start_waypoint = builder.nav(
patrol_start, ingress_egress_altitude, use_agl_patrol_altitude
)
patrol_start_waypoint.name = "FLOT START"
patrol_start_waypoint.pretty_name = "FLOT start"
patrol_start_waypoint.description = "FLOT boundary"
patrol_end_waypoint = builder.nav(
patrol_end, ingress_egress_altitude, use_agl_patrol_altitude
)
patrol_end_waypoint.name = "FLOT END"
patrol_end_waypoint.pretty_name = "FLOT end"
patrol_end_waypoint.description = "FLOT boundary"
ingress = builder.ingress(
FlightWaypointType.INGRESS_CAS, ingress_point, location
)
ingress.description = f"Ingress to provide CAS at {location}"
return CasLayout(
departure=builder.takeoff(self.flight.departure),
nav_to=builder.nav_path(
self.flight.departure.position,
ingress,
ingress_point,
ingress_egress_altitude,
use_agl_ingress_egress,
use_agl_patrol_altitude,
),
nav_from=builder.nav_path(
egress,
patrol_end,
self.flight.arrival.position,
ingress_egress_altitude,
use_agl_ingress_egress,
use_agl_patrol_altitude,
),
patrol_start=builder.ingress(
FlightWaypointType.INGRESS_CAS, ingress, location
),
target=builder.cas(center),
patrol_end=builder.egress(egress, location),
ingress=ingress,
patrol_start=patrol_start_waypoint,
patrol_end=patrol_end_waypoint,
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
custom_waypoints=list(),
)
def build(self) -> CasFlightPlan:
return CasFlightPlan(self.flight, self.layout())
def build(self, dump_debug_info: bool = False) -> CasFlightPlan:
return CasFlightPlan(self.flight, self.layout(dump_debug_info))

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
@@ -17,8 +17,6 @@ if TYPE_CHECKING:
@dataclass
class CustomLayout(Layout):
custom_waypoints: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.custom_waypoints
@@ -29,6 +27,10 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
def builder_type() -> Type[Builder]:
return Builder
@property
def is_custom(self) -> bool:
return True
@property
def tot_waypoint(self) -> FlightWaypoint:
target_types = (
@@ -42,16 +44,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 + self.tot_offset
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
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
@@ -65,8 +71,8 @@ class Builder(IBuilder[CustomFlightPlan, CustomLayout]):
self.waypoints = waypoints
def layout(self) -> CustomLayout:
builder = WaypointBuilder(self.flight, self.coalition)
builder = WaypointBuilder(self.flight)
return CustomLayout(builder.takeoff(self.flight.departure), self.waypoints)
def build(self) -> CustomFlightPlan:
def build(self, dump_debug_info: bool = False) -> CustomFlightPlan:
return CustomFlightPlan(self.flight, self.layout())

View File

@@ -37,5 +37,5 @@ class Builder(FormationAttackBuilder[DeadFlightPlan, FormationAttackLayout]):
return self._build(FlightWaypointType.INGRESS_DEAD)
def build(self) -> DeadFlightPlan:
def build(self, dump_debug_info: bool = False) -> DeadFlightPlan:
return DeadFlightPlan(self.flight, self.layout())

View File

@@ -2,12 +2,15 @@ from __future__ import annotations
from typing import Type
from .airassault import AirAssaultLayout
from .airlift import AirliftLayout
from .formationattack import (
FormationAttackBuilder,
FormationAttackFlightPlan,
FormationAttackLayout,
)
from .waypointbuilder import WaypointBuilder
from .. import FlightType
class EscortFlightPlan(FormationAttackFlightPlan):
@@ -20,41 +23,81 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
def layout(self) -> FormationAttackLayout:
assert self.package.waypoints is not None
builder = WaypointBuilder(self.flight, self.coalition)
builder = WaypointBuilder(self.flight)
ingress, target = builder.escort(
self.package.waypoints.ingress, self.package.target
)
ingress.only_for_player = True
target.only_for_player = True
hold = builder.hold(self._hold_point())
join = builder.join(self.package.waypoints.join)
split = builder.split(self.package.waypoints.split)
refuel = builder.refuel(self.package.waypoints.refuel)
hold = None
if not self.flight.is_helo:
hold = builder.hold(self._hold_point())
join_pos = (
self.package.waypoints.ingress
if self.flight.is_helo
else self.package.waypoints.join
)
join = builder.join(join_pos)
split = builder.split(self._get_split())
is_helo = builder.flight.is_helo
initial = builder.escort_hold(
self.package.waypoints.initial, self.doctrine.ingress_altitude
target.position if is_helo else self.package.waypoints.initial,
)
pf = self.package.primary_flight
if pf and pf.flight_type in [FlightType.AIR_ASSAULT, FlightType.TRANSPORT]:
layout = pf.flight_plan.layout
assert isinstance(layout, AirAssaultLayout) or isinstance(
layout, AirliftLayout
)
if isinstance(layout, AirliftLayout):
ascent = layout.pickup_ascent or layout.drop_off_ascent
assert ascent is not None
join = builder.join(ascent.position)
if layout.pickup and layout.drop_off_ascent:
join = builder.join(layout.drop_off_ascent.position)
elif layout.pickup:
join = builder.join(layout.pickup.position)
split = builder.split(layout.arrival.position)
if layout.drop_off:
initial = builder.escort_hold(
layout.drop_off.position,
)
refuel = self._build_refuel(builder)
departure = builder.takeoff(self.flight.departure)
nav_to = builder.nav_path(
hold.position if hold else departure.position,
join.position,
builder.get_cruise_altitude,
)
nav_from = builder.nav_path(
refuel.position if refuel else split.position,
self.flight.arrival.position,
builder.get_cruise_altitude,
)
return FormationAttackLayout(
departure=builder.takeoff(self.flight.departure),
departure=departure,
hold=hold,
nav_to=builder.nav_path(
hold.position, join.position, self.doctrine.ingress_altitude
),
nav_to=nav_to,
join=join,
ingress=ingress,
initial=initial,
targets=[target],
split=split,
refuel=refuel,
nav_from=builder.nav_path(
refuel.position,
self.flight.arrival.position,
self.doctrine.ingress_altitude,
),
nav_from=nav_from,
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
custom_waypoints=list(),
)
def build(self) -> EscortFlightPlan:
def build(self, dump_debug_info: bool = False) -> EscortFlightPlan:
return EscortFlightPlan(self.flight, self.layout())

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
@@ -17,23 +17,14 @@ if TYPE_CHECKING:
@dataclass
class FerryLayout(StandardLayout):
nav_to_destination: list[FlightWaypoint]
def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
if super().delete_waypoint(waypoint):
return True
if waypoint in self.nav_to_destination:
self.nav_to_destination.remove(waypoint)
return True
return False
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.nav_to_destination
yield from self.nav_to
yield self.arrival
if self.divert is not None:
yield self.divert
yield self.bullseye
yield from self.custom_waypoints
class FerryFlightPlan(StandardFlightPlan[FerryLayout]):
@@ -45,16 +36,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
@@ -66,17 +61,17 @@ class Builder(IBuilder[FerryFlightPlan, FerryLayout]):
f"{self.flight.departure}"
)
altitude_is_agl = self.flight.unit_type.dcs_unit_type.helicopter
builder = WaypointBuilder(self.flight)
altitude_is_agl = self.flight.is_helo
altitude = (
feet(1500)
feet(self.coalition.game.settings.heli_cruise_alt_agl)
if altitude_is_agl
else self.flight.unit_type.preferred_patrol_altitude
else builder.get_patrol_altitude
)
builder = WaypointBuilder(self.flight, self.coalition)
return FerryLayout(
departure=builder.takeoff(self.flight.departure),
nav_to_destination=builder.nav_path(
nav_to=builder.nav_path(
self.flight.departure.position,
self.flight.arrival.position,
altitude,
@@ -85,7 +80,9 @@ class Builder(IBuilder[FerryFlightPlan, FerryLayout]):
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
nav_from=[],
custom_waypoints=list(),
)
def build(self) -> FerryFlightPlan:
def build(self, dump_debug_info: bool = False) -> FerryFlightPlan:
return FerryFlightPlan(self.flight, self.layout())

View File

@@ -8,11 +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 functools import cached_property
from datetime import datetime, timedelta
from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar
from game.typeguard import self_type_guard
@@ -20,10 +19,9 @@ from game.utils import Distance, Speed, meters
from .planningerror import PlanningError
from ..flightwaypointtype import FlightWaypointType
from ..starttype import StartType
from ..traveltime import GroundSpeed, TravelTime
from ..traveltime import GroundSpeed
if TYPE_CHECKING:
from game.dcs.aircrafttype import FuelConsumption
from game.theater import ControlPoint
from ..flight import Flight
from ..flightwaypoint import FlightWaypoint
@@ -32,18 +30,11 @@ if TYPE_CHECKING:
from .loiter import LoiterFlightPlan
from .patrolling import PatrollingFlightPlan
INGRESS_TYPES = {
FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_ESCORT,
FlightWaypointType.INGRESS_SEAD,
FlightWaypointType.INGRESS_STRIKE,
FlightWaypointType.INGRESS_DEAD,
}
@dataclass
class Layout(ABC):
departure: FlightWaypoint
custom_waypoints: list[FlightWaypoint]
@property
def waypoints(self) -> list[FlightWaypoint]:
@@ -81,15 +72,6 @@ class FlightPlan(ABC, Generic[LayoutT]):
"""A list of all waypoints in the flight plan, in order."""
return list(self.iter_waypoints())
def get_index_of_wpt_by_type(self, wpt_type: FlightWaypointType) -> int:
index = 0
for wpt in self.waypoints:
if wpt and not wpt.only_for_player:
index += 1
if wpt.waypoint_type == wpt_type:
return index
return -1
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
"""Iterates over all waypoints in the flight plan, in order."""
yield from self.layout.iter_waypoints()
@@ -123,6 +105,19 @@ class FlightPlan(ABC, Generic[LayoutT]):
#
# Plus, it's a loiter point so there's no reason to hurry.
factor = 0.75
elif (
self.flight.is_helo
and (
a.waypoint_type == FlightWaypointType.JOIN
or "INGRESS" in a.waypoint_type.name
or a.waypoint_type == FlightWaypointType.CUSTOM
)
and self.package.primary_flight
and not self.package.primary_flight.flight_plan.is_airassault
):
# Helicopter flights should be slowed down between JOIN & INGRESS
# to allow the escort to keep up while engaging targets along the way.
factor = 0.50
# TODO: Adjust if AGL.
# We don't have an exact heightmap, but we should probably be performing
# *some* adjustment for NTTR since the minimum altitude of the map is
@@ -167,42 +162,9 @@ 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
def bingo_fuel(self) -> int:
"""Bingo fuel value for the FlightPlan"""
if (fuel := self.flight.unit_type.fuel_consumption) is not None:
return self._bingo_estimate(fuel)
return self._legacy_bingo_estimate()
def _bingo_estimate(self, fuel: FuelConsumption) -> int:
distance_to_arrival = self.max_distance_from(self.flight.arrival)
fuel_consumed = fuel.cruise * distance_to_arrival.nautical_miles
bingo = fuel_consumed + fuel.min_safe
return math.ceil(bingo / 100) * 100
def _legacy_bingo_estimate(self) -> int:
distance_to_arrival = self.max_distance_from(self.flight.arrival)
bingo = 1000.0 # Minimum Emergency Fuel
bingo += 500 # Visual Traffic
bingo += 15 * distance_to_arrival.nautical_miles
# TODO: Per aircraft tweaks.
if self.flight.divert is not None:
max_divert_distance = self.max_distance_from(self.flight.divert)
bingo += 10 * max_divert_distance.nautical_miles
return round(bingo / 100) * 100
@cached_property
def joker_fuel(self) -> int:
"""Joker fuel value for the FlightPlan"""
return self.bingo_fuel + 1000
def max_distance_from(self, cp: ControlPoint) -> Distance:
"""Returns the farthest waypoint of the flight plan from a ControlPoint.
:arg cp The ControlPoint to measure distance from.
@@ -231,20 +193,37 @@ 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
total += self.total_time_between_waypoints(previous_waypoint, waypoint)
# 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 total_time_between_waypoints(
self, a: FlightWaypoint, b: FlightWaypoint
) -> timedelta:
"""Returns the total time spent between a and b.
The total time between waypoints differs from the travel time in that it may
include additional time for actions such as loitering.
"""
return self.travel_time_between_waypoints(a, b)
def travel_time_between_waypoints(
self, a: FlightWaypoint, b: FlightWaypoint
) -> timedelta:
return TravelTime.between_points(
a.position, b.position, self.speed_between_waypoints(a, b)
)
error_factor = 1.05
speed = self.speed_between_waypoints(a, b)
distance = meters(a.position.distance_to_point(b.position))
return timedelta(hours=distance.nautical_miles / speed.knots * error_factor)
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:
@@ -267,34 +246,24 @@ 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()
+ self.estimate_takeoff_time()
)
# 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()
- self.estimate_takeoff_time()
)
def estimate_startup(self) -> timedelta:
if self.flight.start_type is StartType.COLD:
@@ -308,13 +277,32 @@ class FlightPlan(ABC, Generic[LayoutT]):
def estimate_ground_ops(self) -> timedelta:
if self.flight.start_type in {StartType.RUNWAY, StartType.IN_FLIGHT}:
return timedelta()
if self.flight.from_cp.is_fleet:
if self.flight.departure.is_fleet or self.flight.departure.is_fob:
return timedelta(minutes=2)
else:
return timedelta(minutes=8)
def estimate_takeoff_time(self) -> timedelta:
if self.flight.departure.is_offmap:
return timedelta()
return timedelta(seconds=30)
@property
def mission_departure_time(self) -> timedelta:
def is_airassault(self) -> bool:
return False
@property
@abstractmethod
def mission_begin_on_station_time(self) -> datetime | None:
"""The time that the mission is first on-station."""
...
@property
def is_custom(self) -> bool:
return False
@property
def mission_departure_time(self) -> datetime:
"""The time that the mission is complete and the flight RTBs."""
raise NotImplementedError

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, Optional
@@ -10,7 +10,6 @@ from game.typeguard import self_type_guard
from game.utils import Speed
from .flightplan import FlightPlan
from .loiter import LoiterFlightPlan, LoiterLayout
from ..traveltime import GroundSpeed, TravelTime
if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint
@@ -18,24 +17,16 @@ if TYPE_CHECKING:
@dataclass
class FormationLayout(LoiterLayout, ABC):
nav_to: list[FlightWaypoint]
join: Optional[FlightWaypoint]
join: FlightWaypoint
split: FlightWaypoint
refuel: Optional[FlightWaypoint]
nav_from: list[FlightWaypoint]
def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
if super().delete_waypoint(waypoint):
return True
if waypoint in self.nav_to:
self.nav_to.remove(waypoint)
return True
elif waypoint in self.nav_from:
self.nav_from.remove(waypoint)
return True
elif waypoint == self.refuel:
if waypoint == self.refuel:
self.refuel = None
return True
elif super().delete_waypoint(waypoint):
return True
return False
@@ -84,31 +75,39 @@ 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 + self.tot_offset
return self.join_time
elif waypoint == self.layout.split:
return self.split_time + self.tot_offset
return self.split_time
return None
@property
def push_time(self) -> timedelta:
return self.join_time - TravelTime.between_points(
self.layout.hold.position,
self.layout.join.position,
GroundSpeed.for_flight(self.flight, self.layout.hold.alt),
def push_time(self) -> datetime:
hold2join_time = (
self.travel_time_between_waypoints(
self.layout.hold,
self.layout.join,
)
if self.layout.hold
else timedelta(0)
)
return self.join_time - hold2join_time
@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,14 +3,15 @@ from __future__ import annotations
from abc import ABC
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from typing import TYPE_CHECKING, TypeVar, Optional
from datetime import datetime, timedelta
from typing import Optional
from typing import TYPE_CHECKING, TypeVar
from dcs import Point
from game.flightplan import HoldZoneGeometry
from game.theater import MissionTarget
from game.utils import Speed, meters
from game.utils import Speed, meters, nautical_miles
from .flightplan import FlightPlan
from .formation import FormationFlightPlan, FormationLayout
from .ibuilder import IBuilder
@@ -80,18 +81,18 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
return total
@property
def join_time(self) -> timedelta:
travel_time = self.travel_time_between_waypoints(
def join_time(self) -> datetime:
travel_time = self.total_time_between_waypoints(
self.layout.join, self.layout.ingress
)
return self.ingress_time - travel_time
@property
def split_time(self) -> timedelta:
travel_time_ingress = self.travel_time_between_waypoints(
def split_time(self) -> datetime:
travel_time_ingress = self.total_time_between_waypoints(
self.layout.ingress, self.target_area_waypoint
)
travel_time_egress = self.travel_time_between_waypoints(
travel_time_egress = self.total_time_between_waypoints(
self.target_area_waypoint, self.layout.split
)
minutes_at_target = 0.75 * len(self.layout.targets)
@@ -104,22 +105,22 @@ 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(
travel_time = self.total_time_between_waypoints(
self.layout.ingress, self.target_area_waypoint
)
return tot - travel_time
@property
def initial_time(self) -> timedelta:
def initial_time(self) -> datetime:
tot = self.tot
travel_time = self.travel_time_between_waypoints(
self.layout.initial, 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 == self.layout.initial:
@@ -134,14 +135,16 @@ class FormationAttackLayout(FormationLayout):
ingress: FlightWaypoint
targets: list[FlightWaypoint]
initial: Optional[FlightWaypoint] = None
lineup: Optional[FlightWaypoint] = None
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
if self.hold:
yield self.hold
yield from self.nav_to
if self.join:
yield self.join
yield self.join
if self.lineup:
yield self.lineup
yield self.ingress
if self.initial is not None:
yield self.initial
@@ -154,6 +157,7 @@ class FormationAttackLayout(FormationLayout):
if self.divert is not None:
yield self.divert
yield self.bullseye
yield from self.custom_waypoints
FlightPlanT = TypeVar("FlightPlanT", bound=FlightPlan[FormationAttackLayout])
@@ -167,7 +171,7 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
targets: list[StrikeTarget] | None = None,
) -> FormationAttackLayout:
assert self.package.waypoints is not None
builder = WaypointBuilder(self.flight, self.coalition, targets)
builder = WaypointBuilder(self.flight, targets)
target_waypoints: list[FlightWaypoint] = []
if targets is not None:
@@ -183,18 +187,14 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
)
hold = None
join = None
if (
self.flight is self.package.primary_flight
or self.package.primary_flight
and isinstance(
self.package.primary_flight.flight_plan, FormationAttackFlightPlan
)
):
if not self.flight.is_helo:
hold = builder.hold(self._hold_point())
join = builder.join(self.package.waypoints.join)
split = builder.split(self.package.waypoints.split)
refuel = builder.refuel(self.package.waypoints.refuel)
join_pos = self.package.waypoints.join
if self.flight.is_helo:
join_pos = self.package.waypoints.ingress
join = builder.join(join_pos)
split = builder.split(self._get_split())
refuel = self._build_refuel(builder)
ingress = builder.ingress(
ingress_type, self.package.waypoints.ingress, self.package.target
@@ -206,30 +206,63 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
elif ingress_type == FlightWaypointType.INGRESS_SEAD_SWEEP:
initial = builder.sead_sweep(self.package.target)
lineup = None
if self.flight.flight_type == FlightType.STRIKE:
hdg = self.package.target.position.heading_between_point(ingress.position)
pos = ingress.position.point_from_heading(hdg, nautical_miles(10).meters)
lineup = builder.nav(pos, builder.get_combat_altitude)
is_helo = self.flight.is_helo
ingress_egress_altitude = builder.get_combat_altitude
use_agl_ingress_egress = is_helo
return FormationAttackLayout(
departure=builder.takeoff(self.flight.departure),
hold=hold,
nav_to=builder.nav_path(
hold.position if hold else self.flight.departure.position,
join.position if join else ingress.position,
self.doctrine.ingress_altitude,
ingress_egress_altitude,
use_agl_ingress_egress,
),
join=join,
lineup=lineup,
ingress=ingress,
initial=initial,
targets=target_waypoints,
split=split,
refuel=refuel,
nav_from=builder.nav_path(
refuel.position,
refuel.position if refuel else split.position,
self.flight.arrival.position,
self.doctrine.ingress_altitude,
ingress_egress_altitude,
use_agl_ingress_egress,
),
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
custom_waypoints=list(),
)
def _build_refuel(self, builder: WaypointBuilder) -> Optional[FlightWaypoint]:
refuel: Optional[FlightWaypoint] = None
can_plan = self.flight.coalition.air_wing.can_auto_plan(FlightType.REFUELING)
if not self.flight.is_helo and can_plan and self.package.waypoints:
refuel = builder.refuel(self.package.waypoints.refuel)
return refuel
@property
def primary_flight_is_air_assault(self) -> bool:
if self.flight is self.package.primary_flight:
# Can't call self.package.primary_flight.flight_plan here
# because the flight-plan wasn't created yet.
# Calling the fligh_plan property would result in infinite recursion
return self.flight.flight_type == FlightType.AIR_ASSAULT
else:
assert self.package.primary_flight is not None
fp = self.package.primary_flight.flight_plan
return fp.is_airassault
@staticmethod
def target_waypoint(
flight: Flight, builder: WaypointBuilder, target: StrikeTarget
@@ -265,3 +298,13 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
return HoldZoneGeometry(
target, origin, ip, join, self.coalition, self.theater
).find_best_hold_point()
def _get_split(self) -> Point:
assert self.package.waypoints is not None
assert self.package.primary_flight is not None
split_pos = (
self.package.primary_flight.arrival.position
if self.package.primary_flight.is_helo
else self.package.waypoints.split
)
return split_pos

View File

@@ -25,6 +25,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
def __init__(self, flight: Flight) -> None:
self.flight = flight
self._flight_plan: FlightPlanT | None = None
self.settings = self.flight.coalition.game.settings
def get_or_build(self) -> FlightPlanT:
if self._flight_plan is None:
@@ -32,10 +33,10 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
assert self._flight_plan is not None
return self._flight_plan
def regenerate(self) -> None:
def regenerate(self, dump_debug_info: bool = False) -> None:
try:
self._generate_package_waypoints_if_needed()
self._flight_plan = self.build()
self._generate_package_waypoints_if_needed(dump_debug_info)
self._flight_plan = self.build(dump_debug_info)
except NavMeshError as ex:
color = "blue" if self.flight.squadron.player else "red"
raise PlanningError(
@@ -43,10 +44,15 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
f"{self.flight.departure} to {self.package.target}"
) from ex
def _generate_package_waypoints_if_needed(self) -> None:
if self.package.waypoints is None:
def _generate_package_waypoints_if_needed(self, dump_debug_info: bool) -> None:
# Package waypoints are only valid for offensive missions. Skip this if the
# target is friendly.
if self.package.target.is_friendly(self.is_player):
return
if self.package.waypoints is None or dump_debug_info:
self.package.waypoints = PackageWaypoints.create(
self.package, self.coalition
self.package, self.coalition, dump_debug_info
)
@property
@@ -54,11 +60,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
return self.flight.departure.theater
@abstractmethod
def layout(self) -> LayoutT:
...
@abstractmethod
def build(self) -> FlightPlanT:
def build(self, dump_debug_info: bool = False) -> FlightPlanT:
...
@property

View File

@@ -2,8 +2,9 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import timedelta
from typing import Any, TYPE_CHECKING, TypeGuard, Optional
from datetime import datetime, timedelta
from typing import Any, TYPE_CHECKING, TypeGuard
from typing import Optional
from game.typeguard import self_type_guard
from .flightplan import FlightPlan
@@ -25,18 +26,18 @@ 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 + self.tot_offset
return self.push_time
return None
def travel_time_between_waypoints(
def total_time_between_waypoints(
self, a: FlightWaypoint, b: FlightWaypoint
) -> timedelta:
travel_time = super().travel_time_between_waypoints(a, b)
travel_time = super().total_time_between_waypoints(a, b)
if a != self.layout.hold:
return travel_time
return travel_time + self.hold_duration

View File

@@ -32,5 +32,5 @@ class Builder(FormationAttackBuilder[OcaAircraftFlightPlan, FormationAttackLayou
return self._build(FlightWaypointType.INGRESS_OCA_AIRCRAFT)
def build(self) -> OcaAircraftFlightPlan:
def build(self, dump_debug_info: bool = False) -> OcaAircraftFlightPlan:
return OcaAircraftFlightPlan(self.flight, self.layout())

View File

@@ -32,5 +32,5 @@ class Builder(FormationAttackBuilder[OcaRunwayFlightPlan, FormationAttackLayout]
return self._build(FlightWaypointType.INGRESS_OCA_RUNWAY)
def build(self) -> OcaRunwayFlightPlan:
def build(self, dump_debug_info: bool = False) -> OcaRunwayFlightPlan:
return OcaRunwayFlightPlan(self.flight, self.layout())

View File

@@ -1,11 +1,11 @@
from __future__ import annotations
from datetime import timedelta
from datetime import datetime, timedelta
from typing import Type
from dcs import Point
from game.utils import Distance, Heading, feet, meters
from game.utils import Distance, Heading, meters
from .ibuilder import IBuilder
from .patrolling import PatrollingLayout
from .refuelingflightplan import RefuelingFlightPlan
@@ -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:
@@ -59,10 +59,10 @@ class PackageRefuelingFlightPlan(RefuelingFlightPlan):
"REFUEL", FlightWaypointType.REFUEL, refuel, altitude
)
delay_target_to_split: timedelta = self.travel_time_between_waypoints(
delay_target_to_split: timedelta = self.total_time_between_waypoints(
self.target_area_waypoint(), split_waypoint
)
delay_split_to_refuel: timedelta = self.travel_time_between_waypoints(
delay_split_to_refuel: timedelta = self.total_time_between_waypoints(
split_waypoint, refuel_waypoint
)
@@ -96,13 +96,9 @@ class Builder(IBuilder[PackageRefuelingFlightPlan, PatrollingLayout]):
home_heading.degrees, racetrack_half_distance
)
builder = WaypointBuilder(self.flight, self.coalition)
builder = WaypointBuilder(self.flight)
tanker_type = self.flight.unit_type
if tanker_type.patrol_altitude is not None:
altitude = tanker_type.patrol_altitude
else:
altitude = feet(21000)
altitude = builder.get_patrol_altitude
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
@@ -119,7 +115,8 @@ class Builder(IBuilder[PackageRefuelingFlightPlan, PatrollingLayout]):
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
custom_waypoints=list(),
)
def build(self) -> PackageRefuelingFlightPlan:
def build(self, dump_debug_info: bool = False) -> PackageRefuelingFlightPlan:
return PackageRefuelingFlightPlan(self.flight, self.layout())

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
@@ -18,21 +18,8 @@ if TYPE_CHECKING:
@dataclass
class PatrollingLayout(StandardLayout):
nav_to: list[FlightWaypoint]
patrol_start: FlightWaypoint
patrol_end: FlightWaypoint
nav_from: list[FlightWaypoint]
def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
if super().delete_waypoint(waypoint):
return True
if waypoint in self.nav_to:
self.nav_to.remove(waypoint)
return True
elif waypoint in self.nav_from:
self.nav_from.remove(waypoint)
return True
return False
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
@@ -44,6 +31,7 @@ class PatrollingLayout(StandardLayout):
if self.divert is not None:
yield self.divert
yield self.bullseye
yield from self.custom_waypoints
LayoutT = TypeVar("LayoutT", bound=PatrollingLayout)
@@ -72,22 +60,22 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
"""
@property
def patrol_start_time(self) -> timedelta:
return self.package.time_over_target + self.tot_offset
def patrol_start_time(self) -> datetime:
return self.tot
@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
@@ -101,7 +89,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 | None:
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
@@ -18,16 +18,16 @@ if TYPE_CHECKING:
@dataclass
class RtbLayout(StandardLayout):
abort_location: FlightWaypoint
nav_to_destination: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield self.abort_location
yield from self.nav_to_destination
yield from self.nav_to
yield self.arrival
if self.divert is not None:
yield self.divert
yield self.bullseye
yield from self.custom_waypoints
class RtbFlightPlan(StandardFlightPlan[RtbLayout]):
@@ -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]):
@@ -62,13 +66,13 @@ class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
current_position = self.flight.state.estimate_position()
current_altitude, altitude_reference = self.flight.state.estimate_altitude()
altitude_is_agl = self.flight.unit_type.dcs_unit_type.helicopter
builder = WaypointBuilder(self.flight)
altitude_is_agl = self.flight.is_helo
altitude = (
feet(1500)
feet(self.coalition.game.settings.heli_cruise_alt_agl)
if altitude_is_agl
else self.flight.unit_type.preferred_patrol_altitude
else builder.get_patrol_altitude
)
builder = WaypointBuilder(self.flight, self.flight.coalition)
abort_point = builder.nav(
current_position, current_altitude, altitude_reference == "RADIO"
)
@@ -78,7 +82,7 @@ class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
return RtbLayout(
departure=builder.takeoff(self.flight.departure),
abort_location=abort_point,
nav_to_destination=builder.nav_path(
nav_to=builder.nav_path(
current_position,
self.flight.arrival.position,
altitude,
@@ -87,7 +91,9 @@ class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
nav_from=[],
custom_waypoints=list(),
)
def build(self) -> RtbFlightPlan:
def build(self, dump_debug_info: bool = False) -> RtbFlightPlan:
return RtbFlightPlan(self.flight, self.layout())

View File

@@ -24,5 +24,5 @@ class Builder(FormationAttackBuilder[SeadFlightPlan, FormationAttackLayout]):
def layout(self) -> FormationAttackLayout:
return self._build(FlightWaypointType.INGRESS_SEAD)
def build(self) -> SeadFlightPlan:
def build(self, dump_debug_info: bool = False) -> SeadFlightPlan:
return SeadFlightPlan(self.flight, self.layout())

View File

@@ -24,5 +24,5 @@ class Builder(FormationAttackBuilder[SeadSweepFlightPlan, FormationAttackLayout]
def layout(self) -> FormationAttackLayout:
return self._build(FlightWaypointType.INGRESS_SEAD_SWEEP)
def build(self) -> SeadSweepFlightPlan:
def build(self, dump_debug_info: bool = False) -> SeadSweepFlightPlan:
return SeadSweepFlightPlan(self.flight, self.layout())

View File

@@ -1,10 +1,14 @@
from __future__ import annotations
from abc import ABC
from copy import deepcopy
from dataclasses import dataclass
from typing import TYPE_CHECKING, TypeVar
from typing import TYPE_CHECKING, TypeVar, Optional
from game.ato.flightplans.flightplan import FlightPlan, Layout
from .waypointbuilder import WaypointBuilder
from ..flightwaypointtype import FlightWaypointType
from ...utils import feet
if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint
@@ -15,11 +19,60 @@ class StandardLayout(Layout, ABC):
arrival: FlightWaypoint
divert: FlightWaypoint | None
bullseye: FlightWaypoint
nav_to: list[FlightWaypoint]
nav_from: list[FlightWaypoint]
def add_waypoint(
self, wpt: FlightWaypoint, next_wpt: Optional[FlightWaypoint]
) -> bool:
new_wpt = self.get_midpoint(wpt, next_wpt)
if wpt.waypoint_type in [FlightWaypointType.TAKEOFF, FlightWaypointType.LOITER]:
self.nav_to.insert(0, new_wpt)
return True
elif wpt.waypoint_type in [
FlightWaypointType.SPLIT,
FlightWaypointType.REFUEL,
FlightWaypointType.PATROL,
FlightWaypointType.EGRESS,
]:
self.nav_from.insert(0, new_wpt)
return True
elif wpt.waypoint_type is FlightWaypointType.NAV:
if wpt in self.nav_to:
index = self.nav_to.index(wpt) + 1
self.nav_to.insert(index, new_wpt)
return True
elif wpt in self.nav_from:
index = self.nav_from.index(wpt) + 1
self.nav_from.insert(index, new_wpt)
return True
return False
@staticmethod
def get_midpoint(
wpt: FlightWaypoint, next_wpt: Optional[FlightWaypoint]
) -> FlightWaypoint:
new_pos = deepcopy(wpt.position)
next_alt = feet(20000)
if next_wpt:
new_pos = wpt.position.lerp(next_wpt.position, 0.5)
next_alt = next_wpt.alt
new_wpt = WaypointBuilder.nav(new_pos, max(wpt.alt, next_alt))
return new_wpt
def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
if waypoint is self.divert:
self.divert = None
return True
elif waypoint in self.nav_to:
self.nav_to.remove(waypoint)
return True
elif waypoint in self.nav_from:
self.nav_from.remove(waypoint)
return True
elif waypoint in self.custom_waypoints:
self.custom_waypoints.remove(waypoint)
return True
return False

View File

@@ -11,6 +11,7 @@ from .formationattack import (
from .invalidobjectivelocation import InvalidObjectiveLocation
from .waypointbuilder import StrikeTarget
from ..flightwaypointtype import FlightWaypointType
from ...theater.theatergroup import SceneryUnit
class StrikeFlightPlan(FormationAttackFlightPlan):
@@ -28,9 +29,12 @@ class Builder(FormationAttackBuilder[StrikeFlightPlan, FormationAttackLayout]):
targets: list[StrikeTarget] = []
for idx, unit in enumerate(location.strike_targets):
targets.append(StrikeTarget(f"{unit.type.id} #{idx}", unit))
name = unit.type.id
if isinstance(unit, SceneryUnit):
name = unit.name
targets.append(StrikeTarget(f"{name} #{idx}", unit))
return self._build(FlightWaypointType.INGRESS_STRIKE, targets)
def build(self) -> StrikeFlightPlan:
def build(self, dump_debug_info: bool = False) -> StrikeFlightPlan:
return StrikeFlightPlan(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
@@ -10,7 +10,6 @@ from game.utils import Heading
from .ibuilder import IBuilder
from .loiter import LoiterFlightPlan, LoiterLayout
from .waypointbuilder import WaypointBuilder
from ..traveltime import GroundSpeed, TravelTime
from ...flightplan import HoldZoneGeometry
if TYPE_CHECKING:
@@ -36,6 +35,7 @@ class SweepLayout(LoiterLayout):
if self.divert is not None:
yield self.divert
yield self.bullseye
yield from self.custom_waypoints
class SweepFlightPlan(LoiterFlightPlan):
@@ -55,38 +55,41 @@ class SweepFlightPlan(LoiterFlightPlan):
return -timedelta(minutes=5)
@property
def sweep_start_time(self) -> timedelta:
travel_time = self.travel_time_between_waypoints(
def sweep_start_time(self) -> datetime:
travel_time = self.total_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 + self.tot_offset
return self.sweep_start_time
if waypoint == self.layout.sweep_end:
return self.sweep_end_time + self.tot_offset
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 + self.tot_offset
return self.push_time
return None
@property
def push_time(self) -> timedelta:
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 push_time(self) -> datetime:
return self.sweep_end_time - self.travel_time_between_waypoints(
self.layout.hold,
self.layout.sweep_end,
)
@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.sweep_end_time
@@ -101,27 +104,28 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
heading.degrees, -self.doctrine.sweep_distance.meters
)
builder = WaypointBuilder(self.flight, self.coalition)
start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude)
builder = WaypointBuilder(self.flight)
altitude = builder.get_patrol_altitude
start, end = builder.sweep(start_pos, target, altitude)
hold = builder.hold(self._hold_point())
return SweepLayout(
departure=builder.takeoff(self.flight.departure),
hold=hold,
nav_to=builder.nav_path(
hold.position, start.position, self.doctrine.ingress_altitude
),
nav_to=builder.nav_path(hold.position, start.position, altitude),
nav_from=builder.nav_path(
end.position,
self.flight.arrival.position,
self.doctrine.ingress_altitude,
altitude,
),
sweep_start=start,
sweep_end=end,
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
custom_waypoints=list(),
)
def _hold_point(self) -> Point:
@@ -134,5 +138,5 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
target, origin, ip, join, self.coalition, self.theater
).find_best_hold_point()
def build(self) -> SweepFlightPlan:
def build(self, dump_debug_info: bool = False) -> SweepFlightPlan:
return SweepFlightPlan(self.flight, self.layout())

View File

@@ -1,12 +1,11 @@
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
from game.utils import Distance, Speed
from .capbuilder import CapBuilder
from .patrolling import PatrollingFlightPlan, PatrollingLayout
from .waypointbuilder import WaypointBuilder
@@ -31,6 +30,15 @@ class TarCapLayout(PatrollingLayout):
if self.divert is not None:
yield self.divert
yield self.bullseye
yield from self.custom_waypoints
def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
if waypoint == self.refuel:
self.refuel = None
return True
elif super().delete_waypoint(waypoint):
return True
return False
class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
@@ -63,20 +71,20 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
def default_tot_offset(self) -> timedelta:
return -timedelta(minutes=2)
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
@@ -87,14 +95,9 @@ class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]):
def layout(self) -> TarCapLayout:
location = self.package.target
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
patrol_alt = max(
self.doctrine.min_patrol_altitude,
min(self.doctrine.max_patrol_altitude, randomized_alt),
)
builder = WaypointBuilder(self.flight)
patrol_alt = builder.get_patrol_altitude
builder = WaypointBuilder(self.flight, self.coalition)
orbit0p, orbit1p = self.cap_racetrack_for_objective(location, barcap=False)
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
@@ -120,7 +123,8 @@ class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]):
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
custom_waypoints=list(),
)
def build(self) -> TarCapFlightPlan:
def build(self, dump_debug_info: bool = False) -> TarCapFlightPlan:
return TarCapFlightPlan(self.flight, self.layout())

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from datetime import timedelta
from typing import Type
from game.utils import Heading, feet, meters, nautical_miles
from game.utils import Heading, meters, nautical_miles
from .ibuilder import IBuilder
from .patrolling import PatrollingLayout
from .refuelingflightplan import RefuelingFlightPlan
@@ -37,7 +37,7 @@ class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]):
# Station 70nm outside the threat zone.
threat_buffer = nautical_miles(
self.flight.coalition.game.settings.tanker_threat_buffer_min_distance
self.coalition.game.settings.tanker_threat_buffer_min_distance
)
if self.threat_zones.threatened(location.position):
orbit_distance = distance_to_threat + threat_buffer
@@ -56,13 +56,9 @@ class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]):
orbit_heading.left.degrees, racetrack_half_distance
)
builder = WaypointBuilder(self.flight, self.coalition)
builder = WaypointBuilder(self.flight)
tanker_type = self.flight.unit_type
if tanker_type.patrol_altitude is not None:
altitude = tanker_type.patrol_altitude
else:
altitude = feet(21000)
altitude = builder.get_patrol_altitude
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
@@ -79,7 +75,8 @@ class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]):
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
custom_waypoints=list(),
)
def build(self) -> TheaterRefuelingFlightPlan:
def build(self, dump_debug_info: bool = False) -> TheaterRefuelingFlightPlan:
return TheaterRefuelingFlightPlan(self.flight, self.layout())

View File

@@ -10,11 +10,11 @@ from typing import (
TYPE_CHECKING,
Tuple,
Union,
Literal,
)
from dcs.mapping import Point, Vector2
from game.ato.flightplans._common_ctld import generate_random_ctld_point
from game.ato.flightwaypoint import AltitudeReference, FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType
from game.theater import (
@@ -24,11 +24,11 @@ from game.theater import (
TheaterGroundObject,
TheaterUnit,
)
from game.theater.interfaces.CTLD import CTLD
from game.utils import Distance, meters, nautical_miles, feet
AGL_TRANSITION_ALT = 5000
if TYPE_CHECKING:
from game.coalition import Coalition
from game.transfers import MultiGroupTransport
from game.theater.theatergroup import TheaterGroup
from game.ato.flight import Flight
@@ -44,20 +44,43 @@ class WaypointBuilder:
def __init__(
self,
flight: Flight,
coalition: Coalition,
targets: Optional[List[StrikeTarget]] = None,
) -> None:
coalition = flight.coalition
self.flight = flight
self.doctrine = coalition.doctrine
self.threat_zones = coalition.opponent.threat_zone
self.navmesh = coalition.nav_mesh
self.targets = targets
self._bullseye = coalition.bullseye
self.settings = self.flight.coalition.game.settings
@property
def is_helo(self) -> bool:
return self.flight.is_helo
@property
def get_patrol_altitude(self) -> Distance:
return self.get_altitude(self.flight.unit_type.preferred_patrol_altitude)
@property
def get_cruise_altitude(self) -> Distance:
return self.get_altitude(self.flight.unit_type.preferred_cruise_altitude)
@property
def get_combat_altitude(self) -> Distance:
return self.get_altitude(self.flight.unit_type.preferred_combat_altitude)
def get_altitude(self, alt: Distance) -> Distance:
randomized_alt = feet(round(alt.feet + self.flight.plane_altitude_offset))
altitude = max(
self.doctrine.min_combat_altitude,
min(self.doctrine.max_combat_altitude, randomized_alt),
)
return (
feet(self.settings.heli_combat_alt_agl) if self.flight.is_helo else altitude
)
def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
"""Create takeoff waypoint for the given arrival airfield or carrier.
@@ -74,7 +97,7 @@ class WaypointBuilder:
"NAV",
FlightWaypointType.NAV,
position,
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
self.get_cruise_altitude,
description="Enter theater",
pretty_name="Enter theater",
)
@@ -101,7 +124,7 @@ class WaypointBuilder:
"NAV",
FlightWaypointType.NAV,
position,
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
self.get_cruise_altitude,
description="Exit theater",
pretty_name="Exit theater",
)
@@ -127,12 +150,10 @@ class WaypointBuilder:
return None
position = divert.position
altitude_type: AltitudeReference
altitude_type: AltitudeReference = "BARO"
if isinstance(divert, OffMapSpawn):
altitude = (
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude
)
altitude_type = "BARO"
altitude = self.get_cruise_altitude
altitude_type = "RADIO" if self.is_helo else altitude_type
else:
altitude = meters(0)
altitude_type = "RADIO"
@@ -162,14 +183,15 @@ class WaypointBuilder:
def hold(self, position: Point) -> FlightWaypoint:
alt_type: AltitudeReference = "BARO"
if self.is_helo:
if self.is_helo or self.get_combat_altitude.feet <= AGL_TRANSITION_ALT:
alt_type = "RADIO"
return FlightWaypoint(
"HOLD",
FlightWaypointType.LOITER,
position,
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
# TODO: dedicated altitude setting for holding
self.get_cruise_altitude if self.is_helo else self.get_combat_altitude,
alt_type,
description="Wait until push time",
pretty_name="Hold",
@@ -177,14 +199,14 @@ class WaypointBuilder:
def join(self, position: Point) -> FlightWaypoint:
alt_type: AltitudeReference = "BARO"
if self.is_helo:
if self.is_helo or self.get_cruise_altitude.feet <= AGL_TRANSITION_ALT:
alt_type = "RADIO"
return FlightWaypoint(
"JOIN",
FlightWaypointType.JOIN,
position,
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
self.get_cruise_altitude,
alt_type,
description="Rendezvous with package",
pretty_name="Join",
@@ -192,14 +214,14 @@ class WaypointBuilder:
def refuel(self, position: Point) -> FlightWaypoint:
alt_type: AltitudeReference = "BARO"
if self.is_helo:
if self.is_helo or self.get_cruise_altitude.feet <= AGL_TRANSITION_ALT:
alt_type = "RADIO"
return FlightWaypoint(
"REFUEL",
FlightWaypointType.REFUEL,
position,
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
self.get_cruise_altitude,
alt_type,
description="Refuel from tanker",
pretty_name="Refuel",
@@ -207,14 +229,14 @@ class WaypointBuilder:
def split(self, position: Point) -> FlightWaypoint:
alt_type: AltitudeReference = "BARO"
if self.is_helo:
if self.is_helo or self.get_combat_altitude.feet <= AGL_TRANSITION_ALT:
alt_type = "RADIO"
return FlightWaypoint(
"SPLIT",
FlightWaypointType.SPLIT,
position,
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
self.get_combat_altitude,
alt_type,
description="Depart from package",
pretty_name="Split",
@@ -226,16 +248,26 @@ class WaypointBuilder:
position: Point,
objective: MissionTarget,
) -> FlightWaypoint:
alt = self.doctrine.ingress_altitude
alt = self.get_combat_altitude
alt_type: AltitudeReference = "BARO"
if self.is_helo or self.flight.is_hercules:
alt_type = "RADIO"
alt = meters(60) if self.is_helo else feet(1000)
alt = (
feet(self.flight.coalition.game.settings.heli_combat_alt_agl)
if self.is_helo
else feet(1000)
)
elif alt.feet <= AGL_TRANSITION_ALT:
alt_type = "RADIO"
heading = objective.position.heading_between_point(position)
return FlightWaypoint(
"INGRESS",
ingress_type,
position,
objective.position.point_from_heading(heading, nautical_miles(5).meters)
if self.is_helo
else position,
alt,
alt_type,
description=f"INGRESS on {objective.name}",
@@ -245,14 +277,14 @@ class WaypointBuilder:
def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint:
alt_type: AltitudeReference = "BARO"
if self.is_helo:
if self.is_helo or self.get_combat_altitude.feet <= AGL_TRANSITION_ALT:
alt_type = "RADIO"
return FlightWaypoint(
"EGRESS",
FlightWaypointType.EGRESS,
position,
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
self.get_combat_altitude,
alt_type,
description=f"EGRESS from {target.name}",
pretty_name=f"EGRESS from {target.name}",
@@ -290,11 +322,15 @@ class WaypointBuilder:
return self._target_area(f"STRIKE {target.name}", target)
def sead_area(self, target: MissionTarget) -> FlightWaypoint:
alt_type: AltitudeReference = "BARO"
if self.get_combat_altitude.feet <= AGL_TRANSITION_ALT:
alt_type = "RADIO"
return self._target_area(
f"SEAD on {target.name}",
target,
altitude=self.doctrine.ingress_altitude,
alt_type="BARO",
altitude=self.get_combat_altitude,
alt_type=alt_type,
)
def dead_area(self, target: MissionTarget) -> FlightWaypoint:
@@ -349,7 +385,9 @@ class WaypointBuilder:
"CAS",
FlightWaypointType.CAS,
position,
meters(60) if self.is_helo else meters(1000),
feet(self.flight.coalition.game.settings.heli_combat_alt_agl)
if self.is_helo
else meters(1000),
"RADIO",
description="Provide CAS",
pretty_name="CAS",
@@ -363,11 +401,13 @@ class WaypointBuilder:
position: Position of the waypoint.
altitude: Altitude of the racetrack.
"""
baro: AltitudeReference = "BARO"
return FlightWaypoint(
"RACETRACK START",
FlightWaypointType.PATROL_TRACK,
position,
altitude,
"RADIO" if altitude.feet <= AGL_TRANSITION_ALT else baro,
description="Orbit between this point and the next point",
pretty_name="Race-track start",
)
@@ -380,11 +420,13 @@ class WaypointBuilder:
position: Position of the waypoint.
altitude: Altitude of the racetrack.
"""
baro: AltitudeReference = "BARO"
return FlightWaypoint(
"RACETRACK END",
FlightWaypointType.PATROL,
position,
altitude,
"RADIO" if altitude.feet <= AGL_TRANSITION_ALT else baro,
description="Orbit between this point and the previous point",
pretty_name="Race-track end",
)
@@ -412,38 +454,39 @@ class WaypointBuilder:
start: Position of the waypoint.
altitude: Altitude of the racetrack.
"""
baro: AltitudeReference = "BARO"
return FlightWaypoint(
"ORBIT",
FlightWaypointType.LOITER,
start,
altitude,
"RADIO" if altitude.feet <= AGL_TRANSITION_ALT else baro,
description="Anchor and hold at this point",
pretty_name="Orbit",
)
def sead_search(self, target: MissionTarget) -> FlightWaypoint:
hold = self._sead_search_point(target)
baro: AltitudeReference = "BARO"
return FlightWaypoint(
"SEAD Search",
FlightWaypointType.NAV,
hold,
self.doctrine.ingress_altitude,
alt_type="BARO",
self.get_combat_altitude,
"RADIO" if self.get_combat_altitude.feet <= AGL_TRANSITION_ALT else baro,
description="Anchor and search from this point",
pretty_name="SEAD Search",
)
def sead_sweep(self, target: MissionTarget) -> FlightWaypoint:
hold = self._sead_search_point(target)
baro: AltitudeReference = "BARO"
return FlightWaypoint(
"SEAD Sweep",
FlightWaypointType.NAV,
hold,
self.doctrine.ingress_altitude,
alt_type="BARO",
self.get_combat_altitude,
"RADIO" if self.get_combat_altitude.feet <= AGL_TRANSITION_ALT else baro,
description="Anchor and search from this point",
pretty_name="SEAD Sweep",
)
@@ -471,20 +514,24 @@ class WaypointBuilder:
)
return hold
@staticmethod
def escort_hold(start: Point, altitude: Distance) -> FlightWaypoint:
def escort_hold(self, start: Point) -> FlightWaypoint:
"""Creates custom waypoint for escort flights that need to hold.
Args:
start: Position of the waypoint.
altitude: Altitude of the holding pattern.
"""
altitude = self.get_combat_altitude
alt_type: Literal["BARO", "RADIO"] = "BARO"
if self.is_helo or altitude.feet <= AGL_TRANSITION_ALT:
alt_type = "RADIO"
return FlightWaypoint(
"ESCORT HOLD",
FlightWaypointType.CUSTOM,
start,
altitude,
alt_type=alt_type,
description="Anchor and hold at this point",
pretty_name="Escort Hold",
)
@@ -497,11 +544,13 @@ class WaypointBuilder:
position: Position of the waypoint.
altitude: Altitude of the sweep in meters.
"""
baro: AltitudeReference = "BARO"
return FlightWaypoint(
"SWEEP START",
FlightWaypointType.INGRESS_SWEEP,
position,
altitude,
"RADIO" if altitude.feet <= AGL_TRANSITION_ALT else baro,
description="Proceed to the target and engage enemy aircraft",
pretty_name="Sweep start",
)
@@ -514,11 +563,13 @@ class WaypointBuilder:
position: Position of the waypoint.
altitude: Altitude of the sweep in meters.
"""
baro: AltitudeReference = "BARO"
return FlightWaypoint(
"SWEEP END",
FlightWaypointType.EGRESS,
position,
altitude,
"RADIO" if altitude.feet <= AGL_TRANSITION_ALT else baro,
description="End of sweep",
pretty_name="Sweep end",
)
@@ -547,7 +598,7 @@ class WaypointBuilder:
target: The mission target.
"""
alt_type: AltitudeReference = "BARO"
if self.is_helo:
if self.is_helo or self.get_combat_altitude.feet <= AGL_TRANSITION_ALT:
alt_type = "RADIO"
# This would preferably be no points at all, and instead the Escort task
@@ -561,7 +612,7 @@ class WaypointBuilder:
"TARGET",
FlightWaypointType.TARGET_GROUP_LOC,
target.position,
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
self.get_combat_altitude,
alt_type,
description="Escort the package",
pretty_name="Target area",
@@ -583,17 +634,19 @@ class WaypointBuilder:
pretty_name="Pick-up zone",
)
@staticmethod
def dropoff_zone(drop_off: MissionTarget) -> FlightWaypoint:
def dropoff_zone(self, drop_off: MissionTarget) -> FlightWaypoint:
"""Creates a dropoff landing zone waypoint
This waypoint is used to generate the Trigger Zone used for AirAssault and
AirLift using the CTLD plugin (see LogisticsGenerator)
"""
heli_alt = feet(self.flight.coalition.game.settings.heli_cruise_alt_agl)
altitude = heli_alt if self.flight.is_helo else meters(0)
return FlightWaypoint(
"DROPOFFZONE",
FlightWaypointType.DROPOFF_ZONE,
drop_off.position,
meters(0),
altitude,
"RADIO",
description=f"Drop off cargo at {drop_off.name}",
pretty_name="Drop-off zone",
@@ -604,14 +657,10 @@ class WaypointBuilder:
"""Creates a cargo stop waypoint.
This waypoint is used by AirLift as a landing and stopover waypoint
"""
if isinstance(control_point, CTLD) and control_point.ctld_zones:
pos = generate_random_ctld_point(control_point)
else:
pos = control_point.position
return FlightWaypoint(
"CARGOSTOP",
FlightWaypointType.CARGO_STOP,
pos,
control_point.position,
meters(0),
"RADIO",
description=f"Stop for cargo at {control_point.name}",
@@ -631,7 +680,7 @@ class WaypointBuilder:
altitude_is_agl: True for altitude is AGL. False if altitude is MSL.
"""
alt_type: AltitudeReference = "BARO"
if altitude_is_agl:
if altitude_is_agl or altitude.feet <= AGL_TRANSITION_ALT:
alt_type = "RADIO"
return FlightWaypoint(

View File

@@ -1,29 +1,34 @@
from __future__ import annotations
from collections.abc import Iterator
from typing import Optional, TYPE_CHECKING
from game.ato.iflightroster import IFlightRoster
if TYPE_CHECKING:
from game.squadrons import Squadron, Pilot
class FlightRoster:
class FlightRoster(IFlightRoster):
def __init__(self, squadron: Squadron, initial_size: int = 0) -> None:
self.squadron = squadron
self._squadron = squadron
self.pilots: list[Optional[Pilot]] = []
self.resize(initial_size)
@property
def squadron(self) -> Squadron:
return self._squadron
def iter_pilots(self) -> Iterator[Pilot | None]:
yield from self.pilots
def pilot_at(self, idx: int) -> Pilot | None:
return self.pilots[idx]
@property
def max_size(self) -> int:
return len(self.pilots)
@property
def player_count(self) -> int:
return len([p for p in self.pilots if p is not None and p.player])
@property
def missing_pilots(self) -> int:
return len([p for p in self.pilots if p is None])
def resize(self, new_size: int) -> None:
if self.max_size > new_size:
self.squadron.return_pilots(

View File

@@ -21,6 +21,39 @@ class FlightState(ABC):
self.settings = settings
self.avoid_further_combat = False
def reinitialize(self, now: datetime) -> None:
from game.ato.flightstate import WaitingForStart
start_time = self.flight.flight_plan.startup_time()
if start_time <= now:
self._set_active_flight_state(start_time)
else:
self.flight.set_state(
WaitingForStart(self.flight, self.settings, start_time)
)
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

@@ -26,6 +26,7 @@ class InFlight(FlightState, ABC):
settings: Settings,
waypoint_index: int,
has_aborted: bool = False,
elapsed_time: timedelta = timedelta(),
) -> None:
super().__init__(flight, settings)
waypoints = self.flight.flight_plan.waypoints
@@ -35,7 +36,7 @@ class InFlight(FlightState, ABC):
# TODO: Error checking for flight plans without landing waypoints.
self.next_waypoint = waypoints[self.waypoint_index + 1]
self.total_time_to_next_waypoint = self.travel_time_between_waypoints()
self.elapsed_time = timedelta()
self.elapsed_time = elapsed_time
self.current_waypoint_elapsed = False
@property
@@ -51,17 +52,9 @@ class InFlight(FlightState, ABC):
return index <= self.waypoint_index
def travel_time_between_waypoints(self) -> timedelta:
travel_time = self.flight.flight_plan.travel_time_between_waypoints(
return self.flight.flight_plan.travel_time_between_waypoints(
self.current_waypoint, self.next_waypoint
)
if self.current_waypoint.waypoint_type is FlightWaypointType.LOITER:
# Loiter time is already built into travel_time_between_waypoints. If we're
# at a loiter point but still a regular InFlight (Loiter overrides this
# method) that means we're traveling from the loiter point but no longer
# loitering.
assert self.flight.flight_plan.is_loiter(self.flight.flight_plan)
travel_time -= self.flight.flight_plan.hold_duration
return travel_time
@abstractmethod
def estimate_position(self) -> Point:

View File

@@ -29,6 +29,11 @@ class Navigating(InFlight):
events.update_flight_position(self.flight, self.estimate_position())
def progress(self) -> float:
# if next waypoint is very close, assume we reach it immediately to avoid divide
# by zero error
if self.total_time_to_next_waypoint.total_seconds() < 1:
return 1.0
return (
self.elapsed_time.total_seconds()
/ self.total_time_to_next_waypoint.total_seconds()

View File

@@ -24,7 +24,7 @@ class StartUp(AtDeparture):
) -> None:
if time < self.completion_time:
return
self.flight.set_state(Taxi(self.flight, self.settings, time))
self.flight.set_state(Taxi(self.flight, self.settings, self.completion_time))
@property
def is_waiting_for_start(self) -> bool:

View File

@@ -19,14 +19,19 @@ class Takeoff(AtDeparture):
def __init__(self, flight: Flight, settings: Settings, now: datetime) -> None:
super().__init__(flight, settings)
# TODO: Not accounted for in FlightPlan, can cause discrepancy without loiter.
self.completion_time = now + timedelta(seconds=30)
self.completion_time = now + flight.flight_plan.estimate_takeoff_time()
def on_game_tick(
self, events: GameUpdateEvents, time: datetime, duration: timedelta
) -> None:
if time < self.completion_time:
return
self.flight.set_state(Navigating(self.flight, self.settings, waypoint_index=0))
rollover = time - self.completion_time
self.flight.set_state(
Navigating(
self.flight, self.settings, waypoint_index=0, elapsed_time=rollover
)
)
@property
def is_waiting_for_start(self) -> bool:
@@ -45,7 +50,8 @@ class Takeoff(AtDeparture):
def should_halt_sim(self) -> bool:
if (
self.flight.client_count > 0
and self.settings.player_mission_interrupts_sim_at is StartType.RUNWAY
and self.settings.player_mission_interrupts_sim_at
is [StartType.COLD, StartType.WARM, StartType.RUNWAY]
):
logging.info(
f"Interrupting simulation because {self.flight} has players and has "

View File

@@ -24,7 +24,7 @@ class Taxi(AtDeparture):
) -> None:
if time < self.completion_time:
return
self.flight.set_state(Takeoff(self.flight, self.settings, time))
self.flight.set_state(Takeoff(self.flight, self.settings, self.completion_time))
@property
def is_waiting_for_start(self) -> bool:
@@ -37,7 +37,8 @@ class Taxi(AtDeparture):
def should_halt_sim(self) -> bool:
if (
self.flight.client_count > 0
and self.settings.player_mission_interrupts_sim_at is StartType.WARM
and self.settings.player_mission_interrupts_sim_at
in [StartType.COLD, StartType.WARM]
):
logging.info(
f"Interrupting simulation because {self.flight} has players and has "

View File

@@ -20,7 +20,8 @@ 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:
@@ -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

@@ -95,6 +95,10 @@ class FlightType(Enum):
FlightType.SEAD_SWEEP,
}
@property
def is_escort_type(self) -> bool:
return self in {FlightType.ESCORT, FlightType.SEAD_ESCORT}
@property
def entity_type(self) -> AirEntity:
return {

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import timedelta
from datetime import datetime
from typing import Literal, TYPE_CHECKING
from dcs import Point
@@ -43,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

@@ -25,7 +25,7 @@ class FlightWaypointType(IntEnum):
INGRESS_STRIKE = 5 # Ingress strike (For generator, means that this should have bombing on next TARGET_POINT points)
INGRESS_SEAD = 6 # Ingress sead (For generator, means that this should attack groups on TARGET_GROUP_LOC points)
INGRESS_CAS = 7 # Ingress cas (should start CAS task)
CAS = 8 # Should do CAS there
CAS = 8 # Unused.
EGRESS = 9 # Should stop attack
DESCENT_POINT = 10 # Should start descending to pattern alt
LANDING_POINT = 11 # Should land there

39
game/ato/iflightroster.py Normal file
View File

@@ -0,0 +1,39 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Optional, TYPE_CHECKING, Iterator
if TYPE_CHECKING:
from game.squadrons import Pilot, Squadron
class IFlightRoster(ABC):
@abstractmethod
def iter_pilots(self) -> Iterator[Pilot | None]:
...
@abstractmethod
def pilot_at(self, idx: int) -> Pilot | None:
...
@property
@abstractmethod
def squadron(self) -> Squadron:
...
@property
@abstractmethod
def max_size(self) -> int:
...
@abstractmethod
def resize(self, new_size: int) -> None:
...
@abstractmethod
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
...
@abstractmethod
def clear(self) -> None:
...

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import copy
import datetime
import logging
from collections.abc import Iterable
@@ -10,6 +11,7 @@ from dcs.unittype import FlyingType
from game.data.weapons import Pylon, Weapon, WeaponType
from game.dcs.aircrafttype import AircraftType
from .flighttype import FlightType
from ..persistency import prefer_liberation_payloads
if TYPE_CHECKING:
from .flight import Flight
@@ -35,6 +37,11 @@ class Loadout:
def derive_custom(self, name: str) -> Loadout:
return Loadout(name, self.pylons, self.date, is_custom=True)
def clone(self) -> Loadout:
return Loadout(
self.name, dict(self.pylons), copy.deepcopy(self.date), self.is_custom
)
def has_weapon_of_type(self, weapon_type: WeaponType) -> bool:
for weapon in self.pylons.values():
if weapon is not None and weapon.weapon_group.type is weapon_type:
@@ -155,7 +162,10 @@ class Loadout:
# last - the first element in the tuple will be tried first, then the second,
# etc.
loadout_names = {
t: [f"Retribution {t.value}", f"Liberation {t.value}"] for t in FlightType
t: [f"Liberation {t.value}", f"Retribution {t.value}"]
if prefer_liberation_payloads()
else [f"Retribution {t.value}", f"Liberation {t.value}"]
for t in FlightType
}
legacy_names = {
FlightType.TARCAP: (

View File

@@ -2,7 +2,8 @@ from __future__ import annotations
import logging
from collections import defaultdict
from datetime import timedelta
from copy import deepcopy
from datetime import datetime
from typing import Dict, Optional, TYPE_CHECKING
from game.db import Database
@@ -42,8 +43,11 @@ class Package(RadioFrequencyContainer):
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
@@ -71,7 +75,7 @@ class Package(RadioFrequencyContainer):
# 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()
@@ -90,7 +94,7 @@ class Package(RadioFrequencyContainer):
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()
@@ -112,7 +116,7 @@ class Package(RadioFrequencyContainer):
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)
@@ -120,8 +124,8 @@ class Package(RadioFrequencyContainer):
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 add_flight(self, flight: Flight) -> None:
"""Adds a flight to the package."""
@@ -228,8 +232,9 @@ class Package(RadioFrequencyContainer):
@staticmethod
def clone_package(package: Package) -> Package:
clone = Package(package.target, package._db, package.auto_asap)
clone.time_over_target = package.time_over_target
clone.time_over_target = deepcopy(package.time_over_target)
for f in package.flights:
cf = Flight.clone_flight(f)
cf.package = clone
clone.add_flight(cf)
return clone

View File

@@ -2,13 +2,16 @@ from __future__ import annotations
import random
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING
from dcs import Point
from game.ato.flightplans.waypointbuilder import WaypointBuilder
from game.flightplan import IpZoneGeometry, JoinZoneGeometry
from game.flightplan import JoinZoneGeometry
from game.flightplan.ipsolver import IpSolver
from game.flightplan.refuelzonegeometry import RefuelZoneGeometry
from game.persistency import waypoint_debug_directory
from game.utils import dcs_to_shapely_point
from game.utils import nautical_miles
if TYPE_CHECKING:
@@ -25,15 +28,28 @@ class PackageWaypoints:
refuel: Point
@staticmethod
def create(package: Package, coalition: Coalition) -> PackageWaypoints:
def create(
package: Package, coalition: Coalition, dump_debug_info: bool
) -> PackageWaypoints:
origin = package.departure_closest_to_target()
# Start by picking the best IP for the attack.
ingress_point = IpZoneGeometry(
package.target.position,
origin.position,
coalition,
).find_best_ip()
ip_solver = IpSolver(
dcs_to_shapely_point(origin.position),
dcs_to_shapely_point(package.target.position),
coalition.doctrine,
coalition.opponent.threat_zone.all,
)
ip_solver.set_debug_properties(
waypoint_debug_directory() / "IP", coalition.game.theater.terrain
)
ingress_point_shapely = ip_solver.solve()
if dump_debug_info:
ip_solver.dump_debug_info()
ingress_point = origin.position.new_in_same_map(
ingress_point_shapely.x, ingress_point_shapely.y
)
hdg = package.target.position.heading_between_point(ingress_point)
# Generate a waypoint randomly between 7 & 9 NM

View File

@@ -1,18 +1,9 @@
from __future__ import annotations
import math
from datetime import timedelta
from datetime import datetime
from typing import TYPE_CHECKING
from dcs.mapping import Point
from game.utils import (
Distance,
SPEED_OF_SOUND_AT_SEA_LEVEL,
Speed,
mach,
meters,
)
from game.utils import Distance, SPEED_OF_SOUND_AT_SEA_LEVEL, Speed, mach, meters
if TYPE_CHECKING:
from .flight import Flight
@@ -39,16 +30,8 @@ class GroundSpeed:
# as it can at sea level. This probably isn't great assumption, but
# might. be sufficient given the wiggle room. We can come up with
# another heuristic if needed.
cruise_mach = max_speed.mach() * 0.85
return mach(cruise_mach, altitude)
class TravelTime:
@staticmethod
def between_points(a: Point, b: Point, speed: Speed) -> timedelta:
error_factor = 1.05
distance = meters(a.distance_to_point(b))
return timedelta(hours=distance.nautical_miles / speed.knots * error_factor)
cruise_mach = max_speed.mach() * (0.60 if flight.is_helo else 0.85)
return mach(cruise_mach, altitude if not flight.is_helo else meters(0))
# TODO: Most if not all of this should move into FlightPlan.
@@ -56,44 +39,25 @@ 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
flight_time = flight.flight_plan.minimum_duration_from_start_to_tot()
return now + flight_time

View File

@@ -40,11 +40,6 @@ class SquadronConfig:
secondary = [FlightType(s) for s in secondary_raw]
max_size = data.get("size", DEFAULT_SQUADRON_SIZE)
if max_size is None:
raise RuntimeError(
"Squadron's size is defined in campaign but is missing a value:\n"
f"{data}"
)
return SquadronConfig(
FlightType(data["primary"]),

View File

@@ -98,7 +98,7 @@ class DefaultSquadronAssigner:
)
return None
if aircraft not in self.coalition.faction.aircrafts:
if aircraft not in self.coalition.faction.all_aircrafts:
return None
lo = self.coalition.faction.liveries_overrides

View File

@@ -25,7 +25,7 @@ class SquadronDefGenerator:
) -> Optional[SquadronDef]:
aircraft_choice: Optional[AircraftType] = None
for aircraft in AircraftType.priority_list_for_task(task):
if aircraft not in self.faction.aircrafts:
if aircraft not in self.faction.all_aircrafts:
continue
if not control_point.can_operate(aircraft):
continue
@@ -48,6 +48,7 @@ class SquadronDefGenerator:
aircraft=aircraft,
livery=None,
auto_assignable_mission_types=set(aircraft.iter_task_capabilities()),
radio_presets={},
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
@@ -25,6 +26,7 @@ if TYPE_CHECKING:
from .data.doctrine import Doctrine
from .factions.faction import Faction
from .game import Game
from .lasercodes import LaserCodeRegistry
from .sim import GameUpdateEvents
@@ -85,6 +87,10 @@ class Coalition:
assert self._navmesh is not None
return self._navmesh
@property
def laser_code_registry(self) -> LaserCodeRegistry:
return self.game.laser_code_registry
def __getstate__(self) -> dict[str, Any]:
state = self.__dict__.copy()
# Avoid persisting any volatile types that can be deterministically
@@ -177,10 +183,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)
if not is_turn_0 or not self.game.settings.enable_squadron_aircraft_limits:
self.plan_missions()
if not is_turn_0:
self.plan_missions(self.game.conditions.start_time)
self.plan_procurement()
def refund_outstanding_orders(self) -> None:
@@ -196,16 +202,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,14 @@ from enum import Enum, auto
from typing import Optional
from game.ato.flighttype import FlightType
from game.dcs.aircrafttype import AircraftType
from game.theater import MissionTarget
class EscortType(Enum):
AirToAir = auto()
Sead = auto()
Refuel = auto()
@dataclass(frozen=True)
@@ -32,6 +34,8 @@ class ProposedFlight:
#: field is None.
escort_type: Optional[EscortType] = field(default=None)
preferred_type: Optional[AircraftType] = field(default=None)
def __str__(self) -> str:
return f"{self.task} {self.num_aircraft} ship"

View File

@@ -3,8 +3,8 @@ 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.ato.flighttype import FlightType
from game.ato.traveltime import TotEstimator
@@ -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

@@ -140,7 +140,7 @@ class ObjectiveFinder:
def vulnerable_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over friendly CPs that are vulnerable to enemy CPs.
Vulnerability is defined as any enemy CP within threat range of of the
Vulnerability is defined as any enemy CP within threat range of the
CP.
"""
for cp in self.friendly_control_points():
@@ -167,6 +167,24 @@ class ObjectiveFinder:
yield cp
break
def vulnerable_enemy_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over enemy CPs that are vulnerable to Air Assault.
Vulnerability is defined as any unit being alive in the CP's "blocking_capture" groups.
"""
for cp in self.enemy_control_points():
include = True
for tgo in cp.connected_objectives:
if tgo.distance_to(cp) > cp.CAPTURE_DISTANCE.meters:
continue
for u in tgo.units:
if u.is_vehicle and u.alive:
include = False
break
if not include:
break
if include:
yield cp
def oca_targets(self, min_aircraft: int) -> Iterator[ControlPoint]:
parking_type = ParkingType()
parking_type.include_rotary_wing = True

View File

@@ -10,9 +10,10 @@ from ..ato.starttype import StartType
from ..db.database import Database
if TYPE_CHECKING:
from game.dcs.aircrafttype import AircraftType
from game.squadrons.airwing import AirWing
from game.ato.closestairfields import ClosestAirfields
from game.dcs.aircrafttype import AircraftType
from game.lasercodes import LaserCodeRegistry
from game.squadrons.airwing import AirWing
from .missionproposals import ProposedFlight
@@ -24,6 +25,7 @@ class PackageBuilder:
location: MissionTarget,
closest_airfields: ClosestAirfields,
air_wing: AirWing,
laser_code_registry: LaserCodeRegistry,
flight_db: Database[Flight],
is_player: bool,
start_type: StartType,
@@ -33,9 +35,10 @@ class PackageBuilder:
self.is_player = is_player
self.package = Package(location, flight_db, auto_asap=asap)
self.air_wing = air_wing
self.laser_code_registry = laser_code_registry
self.start_type = start_type
def plan_flight(self, plan: ProposedFlight) -> bool:
def plan_flight(self, plan: ProposedFlight, ignore_range: bool) -> bool:
"""Allocates aircraft for the given flight and adds them to the package.
If no suitable aircraft are available, False is returned. If the failed
@@ -43,8 +46,16 @@ class PackageBuilder:
caller should return any previously planned flights to the inventory
using release_planned_aircraft.
"""
pf = self.package.primary_flight
heli = pf.is_helo if pf else False
squadron = self.air_wing.best_squadron_for(
self.package.target, plan.task, plan.num_aircraft, this_turn=True
self.package.target,
plan.task,
plan.num_aircraft,
heli,
this_turn=True,
preferred_type=plan.preferred_type,
ignore_range=ignore_range,
)
if squadron is None:
return False
@@ -60,6 +71,11 @@ class PackageBuilder:
start_type,
divert=self.find_divert_field(squadron.aircraft, squadron.location),
)
for member in flight.iter_members():
if member.is_player:
member.assign_tgp_laser_code(
self.laser_code_registry.alloc_laser_code()
)
self.package.add_flight(flight)
return True

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
@@ -80,13 +81,17 @@ class PackageFulfiller:
builder: PackageBuilder,
missing_types: Set[FlightType],
purchase_multiplier: int,
ignore_range: bool = False,
) -> None:
if not builder.plan_flight(flight):
if not builder.plan_flight(flight, ignore_range):
pf = builder.package.primary_flight
heli = pf.is_helo if pf else False
missing_types.add(flight.task)
purchase_order = AircraftProcurementRequest(
near=mission.location,
task_capability=flight.task,
number=flight.num_aircraft * purchase_multiplier,
heli=heli,
)
# Reserves are planned for critical missions, so prioritize those orders
# over aircraft needed for non-critical missions.
@@ -132,13 +137,16 @@ class PackageFulfiller:
self,
mission: ProposedMission,
purchase_multiplier: int,
now: datetime,
tracer: MultiEventTracer,
ignore_range: bool = False,
) -> Optional[Package]:
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
builder = PackageBuilder(
mission.location,
ObjectiveDistanceCache.get_closest_airfields(mission.location),
self.air_wing,
self.coalition.laser_code_registry,
self.flight_db,
self.is_player,
self.default_start_type,
@@ -151,17 +159,17 @@ class PackageFulfiller:
missing_types: Set[FlightType] = set()
escorts = []
for proposed_flight in mission.flights:
if proposed_flight.escort_type is not None:
# Escorts are planned after the primary elements of the package.
# If the package does not need escorts they may be pruned.
escorts.append(proposed_flight)
continue
if not self.air_wing_can_plan(proposed_flight.task):
# This air wing can never plan this mission type because they do not
# have compatible aircraft or squadrons. Skip fulfillment so that we
# don't place the purchase request.
missing_types.add(proposed_flight.task)
break
if proposed_flight.escort_type is not None:
# Escorts are planned after the primary elements of the package.
# If the package does not need escorts they may be pruned.
escorts.append(proposed_flight)
continue
with tracer.trace("Flight planning"):
self.plan_flight(
mission,
@@ -169,6 +177,7 @@ class PackageFulfiller:
builder,
missing_types,
purchase_multiplier,
ignore_range,
)
if missing_types:
@@ -221,6 +230,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

@@ -0,0 +1,15 @@
from collections.abc import Iterator
from game.commander.tasks.primitive.antiship import PlanAntiShip
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
class AttackShips(CompoundTask[TheaterState]):
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
for ship in state.enemy_ships:
# Ammo depots are targeted based on the needs of the front line by
# ReduceEnemyFrontLineCapacity. No reason to target them before that front
# line is active.
if ship.has_live_radar_sam:
yield [PlanAntiShip(ship)]

View File

@@ -7,6 +7,7 @@ from game.commander.tasks.compound.destroyenemygroundunits import (
from game.commander.tasks.compound.reduceenemyfrontlinecapacity import (
ReduceEnemyFrontLineCapacity,
)
from game.commander.tasks.primitive.airassault import PlanAirAssault
from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
@@ -22,6 +23,7 @@ class CaptureBase(CompoundTask[TheaterState]):
yield [DestroyEnemyGroundUnits(self.front_line)]
if self.worth_destroying_ammo_depots(state):
yield [ReduceEnemyFrontLineCapacity(self.enemy_cp(state))]
yield [PlanAirAssault(self.enemy_cp(state))]
def enemy_cp(self, state: TheaterState) -> ControlPoint:
return self.front_line.control_point_hostile_to(state.context.coalition.player)

View File

@@ -6,6 +6,7 @@ from game.commander.tasks.compound.attackairinfrastructure import (
)
from game.commander.tasks.compound.attackbattlepositions import AttackBattlePositions
from game.commander.tasks.compound.attackbuildings import AttackBuildings
from game.commander.tasks.compound.attackships import AttackShips
from game.commander.tasks.compound.capturebases import CaptureBases
from game.commander.tasks.compound.defendbases import DefendBases
from game.commander.tasks.compound.degradeiads import DegradeIads
@@ -25,10 +26,11 @@ class PlanNextAction(CompoundTask[TheaterState]):
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
yield [TheaterSupport()]
yield [ProtectAirSpace()]
yield [CaptureBases()]
yield [DefendBases()]
yield [InterdictReinforcements()]
yield [DegradeIads()]
yield [AttackBattlePositions()]
yield [CaptureBases()]
yield [AttackAirInfrastructure(self.aircraft_cold_start)]
yield [AttackBuildings()]
yield [DegradeIads()]
yield [AttackShips()]

View File

@@ -105,6 +105,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

@@ -11,6 +11,11 @@ from game.theater import MissionTarget
@dataclass
class PlanAewc(PackagePlanningTask[MissionTarget]):
def preconditions_met(self, state: TheaterState) -> bool:
if (
state.context.coalition.player
and not state.context.settings.auto_ato_behavior_awacs
):
return False
if not super().preconditions_met(state):
return False
return self.target in state.aewc_targets

View File

@@ -0,0 +1,26 @@
from __future__ import annotations
from dataclasses import dataclass
from game.ato.flighttype import FlightType
from game.commander.tasks.packageplanningtask import PackagePlanningTask
from game.commander.theaterstate import TheaterState
from game.theater import ControlPoint
@dataclass
class PlanAirAssault(PackagePlanningTask[ControlPoint]):
def preconditions_met(self, state: TheaterState) -> bool:
if self.target not in state.vulnerable_control_points:
return False
if not self.target_area_preconditions_met(state):
return False
return super().preconditions_met(state)
def apply_effects(self, state: TheaterState) -> None:
state.vulnerable_control_points.remove(self.target)
def propose_flights(self) -> None:
size = self.get_flight_size()
self.propose_flight(FlightType.AIR_ASSAULT, size)
self.propose_common_escorts()

View File

@@ -1,9 +1,9 @@
from __future__ import annotations
from dataclasses import dataclass
from random import randint
from game.ato.flighttype import FlightType
from game.commander.missionproposals import EscortType
from game.commander.tasks.packageplanningtask import PackagePlanningTask
from game.commander.theaterstate import TheaterState
from game.transfers import CargoShip
@@ -24,4 +24,4 @@ class PlanAntiShipping(PackagePlanningTask[CargoShip]):
def propose_flights(self) -> None:
size = self.get_flight_size()
self.propose_flight(FlightType.ANTISHIP, size)
self.propose_common_escorts()
self.propose_flight(FlightType.ESCORT, 2, EscortType.AirToAir)

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from dataclasses import dataclass
from game.ato.flighttype import FlightType
from game.commander.missionproposals import EscortType
from game.commander.tasks.packageplanningtask import PackagePlanningTask
from game.commander.theaterstate import TheaterState
from game.theater import FrontLine
@@ -31,5 +32,5 @@ class PlanCas(PackagePlanningTask[FrontLine]):
def propose_flights(self) -> None:
size = self.get_flight_size()
self.propose_flight(FlightType.CAS, size)
self.propose_flight(FlightType.TARCAP, 2)
self.propose_flight(FlightType.SEAD_SWEEP, 2)
self.propose_flight(FlightType.TARCAP, 2, EscortType.AirToAir)
self.propose_flight(FlightType.SEAD_SWEEP, 2, EscortType.Sead)

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from dataclasses import dataclass
from game.ato.flighttype import FlightType
from game.commander.missionproposals import EscortType
from game.commander.tasks.packageplanningtask import PackagePlanningTask
from game.commander.theaterstate import TheaterState
from game.theater.theatergroundobject import IadsGroundObject
@@ -39,8 +40,8 @@ class PlanDead(PackagePlanningTask[IadsGroundObject]):
# also threatened by SAMs. We don't want to include a SEAD escort if the
# package is *only* threatened by the target though. Could be improved, but
# needs a decent refactor to the escort planning to do so.
if self.target.has_live_radar_sam:
self.propose_flight(FlightType.SEAD, 2)
self.propose_common_escorts()
if self.target.has_live_radar_sam:
self.propose_flight(FlightType.SEAD, 2, EscortType.Sead)
if self.target.control_point.coalition.game.settings.autoplan_tankers_for_dead:
self.propose_flight(FlightType.REFUELING, 1)
self.propose_flight(FlightType.REFUELING, 1, EscortType.Refuel)

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from dataclasses import dataclass
from game.ato.flighttype import FlightType
from game.commander.missionproposals import EscortType
from game.commander.tasks.packageplanningtask import PackagePlanningTask
from game.commander.theaterstate import TheaterState
from game.theater import ControlPoint
@@ -25,9 +26,9 @@ class PlanOcaStrike(PackagePlanningTask[ControlPoint]):
def propose_flights(self) -> None:
size = self.get_flight_size()
if self.target.cptype == self.target.cptype.AIRBASE:
self.propose_flight(FlightType.OCA_RUNWAY, size)
self.propose_flight(FlightType.OCA_RUNWAY, size) # type:ignore[unreachable]
if self.aircraft_cold_start:
self.propose_flight(FlightType.OCA_AIRCRAFT, 2)
self.propose_common_escorts()
if self.target.coalition.game.settings.autoplan_tankers_for_oca:
self.propose_flight(FlightType.REFUELING, 1)
self.propose_flight(FlightType.REFUELING, 1, EscortType.Refuel)

View File

@@ -11,6 +11,11 @@ from game.theater import MissionTarget
@dataclass
class PlanRefueling(PackagePlanningTask[MissionTarget]):
def preconditions_met(self, state: TheaterState) -> bool:
if (
state.context.coalition.player
and not state.context.settings.auto_ato_behavior_tankers
):
return False
if not super().preconditions_met(state):
return False
return self.target in state.refueling_targets

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from dataclasses import dataclass
from game.ato.flighttype import FlightType
from game.commander.missionproposals import EscortType
from game.commander.tasks.packageplanningtask import PackagePlanningTask
from game.commander.theaterstate import TheaterState
from game.theater.theatergroundobject import TheaterGroundObject
@@ -22,7 +23,7 @@ class PlanStrike(PackagePlanningTask[TheaterGroundObject]):
def propose_flights(self) -> None:
tgt_count = self.target.alive_unit_count
self.propose_flight(FlightType.STRIKE, min(4, (tgt_count // 2) + 1))
self.propose_flight(FlightType.STRIKE, min(4, (tgt_count // 2) + tgt_count % 2))
self.propose_common_escorts()
if self.target.coalition.game.settings.autoplan_tankers_for_strike:
self.propose_flight(FlightType.REFUELING, 1)
self.propose_flight(FlightType.REFUELING, 1, EscortType.Refuel)

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
@@ -60,6 +62,7 @@ class TheaterState(WorldState["TheaterState"]):
strike_targets: list[TheaterGroundObject]
enemy_barcaps: list[ControlPoint]
threat_zones: ThreatZones
vulnerable_control_points: list[ControlPoint]
def _rebuild_threat_zones(self) -> None:
"""Recreates the theater's threat zones based on the current planned state."""
@@ -133,18 +136,25 @@ class TheaterState(WorldState["TheaterState"]):
# IADS threats so that DegradeIads will consider it a threat later.
threatening_air_defenses=self.threatening_air_defenses,
detecting_air_defenses=self.detecting_air_defenses,
vulnerable_control_points=self.vulnerable_control_points,
)
@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
@@ -181,4 +191,5 @@ class TheaterState(WorldState["TheaterState"]):
strike_targets=list(finder.strike_targets()),
enemy_barcaps=list(game.theater.control_points_for(not player)),
threat_zones=game.threat_zone_for(not player),
vulnerable_control_points=list(finder.vulnerable_enemy_control_points()),
)

View File

@@ -18,14 +18,14 @@ class GroundUnitProcurementRatios:
@dataclass(frozen=True)
class Doctrine:
name: str
cas: bool
cap: bool
sead: bool
strike: bool
antiship: bool
rendezvous_altitude: Distance
#: The minimum distance between the departure airfield and the hold point.
hold_distance: Distance
@@ -44,11 +44,14 @@ class Doctrine:
#: target.
min_ingress_distance: Distance
ingress_altitude: Distance
min_patrol_altitude: Distance
max_patrol_altitude: Distance
pattern_altitude: Distance
min_cruise_altitude: Distance
max_cruise_altitude: Distance
min_combat_altitude: Distance
max_combat_altitude: Distance
#: The duration that CAP flights will remain on-station.
cap_duration: timedelta
@@ -89,21 +92,23 @@ class Doctrine:
MODERN_DOCTRINE = Doctrine(
"modern",
cap=True,
cas=True,
sead=True,
strike=True,
antiship=True,
rendezvous_altitude=feet(25000),
hold_distance=nautical_miles(25),
push_distance=nautical_miles(20),
join_distance=nautical_miles(20),
max_ingress_distance=nautical_miles(45),
min_ingress_distance=nautical_miles(10),
ingress_altitude=feet(20000),
min_patrol_altitude=feet(15000),
max_patrol_altitude=feet(33000),
pattern_altitude=feet(5000),
min_cruise_altitude=feet(10000),
max_cruise_altitude=feet(40000),
min_combat_altitude=feet(1000),
max_combat_altitude=feet(35000),
cap_duration=timedelta(minutes=30),
cap_min_track_length=nautical_miles(15),
cap_max_track_length=nautical_miles(40),
@@ -131,21 +136,23 @@ MODERN_DOCTRINE = Doctrine(
)
COLDWAR_DOCTRINE = Doctrine(
name="coldwar",
cap=True,
cas=True,
sead=True,
strike=True,
antiship=True,
rendezvous_altitude=feet(22000),
hold_distance=nautical_miles(15),
push_distance=nautical_miles(10),
join_distance=nautical_miles(10),
max_ingress_distance=nautical_miles(30),
min_ingress_distance=nautical_miles(10),
ingress_altitude=feet(18000),
min_patrol_altitude=feet(10000),
max_patrol_altitude=feet(24000),
pattern_altitude=feet(5000),
min_cruise_altitude=feet(10000),
max_cruise_altitude=feet(30000),
min_combat_altitude=feet(1000),
max_combat_altitude=feet(25000),
cap_duration=timedelta(minutes=30),
cap_min_track_length=nautical_miles(12),
cap_max_track_length=nautical_miles(24),
@@ -173,6 +180,7 @@ COLDWAR_DOCTRINE = Doctrine(
)
WWII_DOCTRINE = Doctrine(
name="ww2",
cap=True,
cas=True,
sead=False,
@@ -181,13 +189,14 @@ WWII_DOCTRINE = Doctrine(
hold_distance=nautical_miles(10),
push_distance=nautical_miles(5),
join_distance=nautical_miles(5),
rendezvous_altitude=feet(10000),
max_ingress_distance=nautical_miles(7),
min_ingress_distance=nautical_miles(5),
ingress_altitude=feet(8000),
min_patrol_altitude=feet(4000),
max_patrol_altitude=feet(15000),
pattern_altitude=feet(5000),
min_cruise_altitude=feet(5000),
max_cruise_altitude=feet(30000),
min_combat_altitude=feet(1000),
max_combat_altitude=feet(10000),
cap_duration=timedelta(minutes=30),
cap_min_track_length=nautical_miles(8),
cap_max_track_length=nautical_miles(18),
@@ -212,3 +221,9 @@ WWII_DOCTRINE = Doctrine(
sead_escort_engagement_range=nautical_miles(10),
escort_engagement_range=nautical_miles(5),
)
ALL_DOCTRINES = [
COLDWAR_DOCTRINE,
MODERN_DOCTRINE,
WWII_DOCTRINE,
]

View File

@@ -38,6 +38,7 @@ TRACK_RADARS = {
AirDefence.Kub_1S91_str,
AirDefence.Snr_s_125_tr,
AirDefence.S_300PS_40B6M_tr,
AirDefence.S_300PS_5H63C_30H6_tr,
AirDefence.Hawk_tr,
AirDefence.Patriot_str,
AirDefence.SNR_75V,
@@ -53,30 +54,36 @@ TRACK_RADARS = {
}
LAUNCHER_TRACKER_PAIRS = {
AirDefence.Kub_2P25_ln: AirDefence.Kub_1S91_str,
AirDefence.X_5p73_s_125_ln: AirDefence.Snr_s_125_tr,
AirDefence.S_300PS_5P85C_ln: AirDefence.S_300PS_40B6M_tr,
AirDefence.S_300PS_5P85D_ln: AirDefence.S_300PS_40B6M_tr,
AirDefence.Hawk_ln: AirDefence.Hawk_tr,
AirDefence.Patriot_ln: AirDefence.Patriot_str,
AirDefence.S_75M_Volhov: AirDefence.SNR_75V,
AirDefence.Rapier_fsa_launcher: AirDefence.Rapier_fsa_blindfire_radar,
AirDefence.HQ_7_LN_SP: AirDefence.HQ_7_STR_SP,
AirDefence.S_200_Launcher: AirDefence.RPC_5N62V,
AirDefence.NASAMS_LN_B: AirDefence.NASAMS_Radar_MPQ64F1,
AirDefence.NASAMS_LN_C: AirDefence.NASAMS_Radar_MPQ64F1,
hds.SAM_SA_2__V759__LN_SM_90: AirDefence.SNR_75V,
hds.SAM_HQ_2_LN_SM_90: AirDefence.SNR_75V,
hds.SAM_SA_3__V_601P__LN_5P73: AirDefence.Snr_s_125_tr,
hds.SAM_SA_10B_S_300PS_5P85SE_LN: hds.SAM_SA_10B_S_300PS_30N6_TR,
hds.SAM_SA_10B_S_300PS_5P85SU_LN: hds.SAM_SA_10B_S_300PS_30N6_TR,
hds.SAM_SA_12_S_300V_9A82_LN: hds.SAM_SA_12_S_300V_9S32_TR,
hds.SAM_SA_12_S_300V_9A83_LN: hds.SAM_SA_12_S_300V_9S32_TR,
hds.SAM_SA_20_S_300PMU1_LN_5P85CE: hds.SAM_SA_20_S_300PMU1_TR_30N6E,
hds.SAM_SA_20_S_300PMU1_LN_5P85DE: hds.SAM_SA_20_S_300PMU1_TR_30N6E,
hds.SAM_SA_20B_S_300PMU2_LN_5P85SE2: hds.SAM_SA_20B_S_300PMU2_TR_92H6E_truck,
hds.SAM_SA_23_S_300VM_9A82ME_LN: hds.SAM_SA_23_S_300VM_9S32ME_TR,
hds.SAM_SA_23_S_300VM_9A83ME_LN: hds.SAM_SA_23_S_300VM_9S32ME_TR,
AirDefence.Kub_2P25_ln: (AirDefence.Kub_1S91_str,),
AirDefence.X_5p73_s_125_ln: (AirDefence.Snr_s_125_tr,),
AirDefence.S_300PS_5P85C_ln: (
AirDefence.S_300PS_40B6M_tr,
AirDefence.S_300PS_5H63C_30H6_tr,
),
AirDefence.S_300PS_5P85D_ln: (
AirDefence.S_300PS_40B6M_tr,
AirDefence.S_300PS_5H63C_30H6_tr,
),
AirDefence.Hawk_ln: (AirDefence.Hawk_tr,),
AirDefence.Patriot_ln: (AirDefence.Patriot_str,),
AirDefence.S_75M_Volhov: (AirDefence.SNR_75V,),
AirDefence.Rapier_fsa_launcher: (AirDefence.Rapier_fsa_blindfire_radar,),
AirDefence.HQ_7_LN_SP: (AirDefence.HQ_7_STR_SP,),
AirDefence.S_200_Launcher: (AirDefence.RPC_5N62V,),
AirDefence.NASAMS_LN_B: (AirDefence.NASAMS_Radar_MPQ64F1,),
AirDefence.NASAMS_LN_C: (AirDefence.NASAMS_Radar_MPQ64F1,),
hds.SAM_SA_2__V759__LN_SM_90: (AirDefence.SNR_75V,),
hds.SAM_HQ_2_LN_SM_90: (AirDefence.SNR_75V,),
hds.SAM_SA_3__V_601P__LN_5P73: (AirDefence.Snr_s_125_tr,),
hds.SAM_SA_10B_S_300PS_5P85SE_LN: (hds.SAM_SA_10B_S_300PS_30N6_TR,),
hds.SAM_SA_10B_S_300PS_5P85SU_LN: (hds.SAM_SA_10B_S_300PS_30N6_TR,),
hds.SAM_SA_12_S_300V_9A82_LN: (hds.SAM_SA_12_S_300V_9S32_TR,),
hds.SAM_SA_12_S_300V_9A83_LN: (hds.SAM_SA_12_S_300V_9S32_TR,),
hds.SAM_SA_20_S_300PMU1_LN_5P85CE: (hds.SAM_SA_20_S_300PMU1_TR_30N6E,),
hds.SAM_SA_20_S_300PMU1_LN_5P85DE: (hds.SAM_SA_20_S_300PMU1_TR_30N6E,),
hds.SAM_SA_20B_S_300PMU2_LN_5P85SE2: (hds.SAM_SA_20B_S_300PMU2_TR_92H6E_truck,),
hds.SAM_SA_23_S_300VM_9A82ME_LN: (hds.SAM_SA_23_S_300VM_9S32ME_TR,),
hds.SAM_SA_23_S_300VM_9A83ME_LN: (hds.SAM_SA_23_S_300VM_9S32ME_TR,),
}
UNITS_WITH_RADAR = {

View File

@@ -10,7 +10,7 @@ from pathlib import Path
from typing import Iterator, Optional, Any, ClassVar
import yaml
from dcs.unitgroup import FlyingGroup
from dcs.flyingunit import FlyingUnit
from dcs.weapons_data import weapon_ids
from game.dcs.aircrafttype import AircraftType
@@ -278,10 +278,10 @@ class Pylon:
# configuration.
return weapon in self.allowed or weapon.clsid == "<CLEAN>"
def equip(self, group: FlyingGroup[Any], weapon: Weapon) -> None:
def equip(self, unit: FlyingUnit, weapon: Weapon) -> None:
if not self.can_equip(weapon):
logging.error(f"Pylon {self.number} cannot equip {weapon.name}")
group.load_pylon(self.make_pydcs_assignment(weapon), self.number)
unit.load_pylon(self.make_pydcs_assignment(weapon), self.number)
def make_pydcs_assignment(self, weapon: Weapon) -> PydcsWeaponAssignment:
return self.number, weapon.pydcs_data

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import inspect
import logging
from collections import defaultdict
from dataclasses import dataclass
@@ -10,11 +11,15 @@ from typing import Any, ClassVar, Dict, Iterator, Optional, TYPE_CHECKING, Type
import yaml
from dcs.helicopters import helicopter_map
from dcs.planes import plane_map
from dcs.unitpropertydescription import UnitPropertyDescription
from dcs.unittype import FlyingType
from dcs.weapons_data import weapon_ids
from game.ato import FlightType
from game.data.units import UnitClass
from game.dcs.unitproperty import UnitProperty
from game.dcs.lasercodeconfig import LaserCodeConfig
from game.dcs.unittype import UnitType
from game.persistency import user_custom_weapon_injections_dir
from game.radio.channels import (
ApacheChannelNamer,
ChannelNamer,
@@ -50,7 +55,6 @@ 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
@@ -128,6 +132,21 @@ class PatrolConfig:
)
@dataclass(frozen=True)
class AltitudesConfig:
cruise: Optional[Distance]
combat: Optional[Distance]
@classmethod
def from_data(cls, data: dict[str, Any]) -> AltitudesConfig:
cruise = data.get("cruise", None)
combat = data.get("combat", None)
return AltitudesConfig(
feet(cruise) if cruise is not None else None,
feet(combat) if combat is not None else None,
)
@dataclass(frozen=True)
class FuelConsumption:
#: The estimated taxi fuel requirement, in pounds.
@@ -178,6 +197,9 @@ class AircraftType(UnitType[Type[FlyingType]]):
patrol_altitude: Optional[Distance]
patrol_speed: Optional[Speed]
cruise_altitude: Optional[Distance]
combat_altitude: Optional[Distance]
#: The maximum range between the origin airfield and the target for which the auto-
#: planner will consider this aircraft usable for a mission.
max_mission_range: Distance
@@ -204,15 +226,25 @@ class AircraftType(UnitType[Type[FlyingType]]):
has_built_in_target_pod: bool
task_priorities: dict[FlightType, int]
laser_code_configs: list[LaserCodeConfig]
use_f15e_waypoint_names: bool
_by_name: ClassVar[dict[str, AircraftType]] = {}
_by_unit_type: ClassVar[dict[type[FlyingType], list[AircraftType]]] = defaultdict(
list
)
def __post_init__(self) -> None:
enrich = {}
for t in self.task_priorities:
if t == FlightType.SEAD:
enrich[FlightType.SEAD_SWEEP] = self.task_priorities[t]
self.task_priorities.update(enrich)
@classmethod
def register(cls, unit_type: AircraftType) -> None:
cls._by_name[unit_type.name] = unit_type
cls._by_name[unit_type.variant_id] = unit_type
cls._by_unit_type[unit_type.dcs_unit_type].append(unit_type)
@property
@@ -231,33 +263,13 @@ class AircraftType(UnitType[Type[FlyingType]]):
def max_speed(self) -> Speed:
return kph(self.dcs_unit_type.max_speed)
@property
@cached_property
def preferred_patrol_altitude(self) -> Distance:
if self.patrol_altitude is not None:
if self.patrol_altitude:
return self.patrol_altitude
else:
# Estimate based on max speed.
# Aircaft with max speed 600 kph will prefer patrol at 10 000 ft
# Aircraft with max speed 2800 kph will prefer pratrol at 33 000 ft
altitude_for_lowest_speed = feet(10 * 1000)
altitude_for_highest_speed = feet(33 * 1000)
lowest_speed = kph(600)
highest_speed = kph(2800)
factor = (self.max_speed - lowest_speed).kph / (
highest_speed - lowest_speed
).kph
altitude = (
altitude_for_lowest_speed
+ (altitude_for_highest_speed - altitude_for_lowest_speed) * factor
)
logging.debug(
f"Preferred patrol altitude for {self.dcs_unit_type.id}: {altitude.feet}"
)
rounded_altitude = feet(round(1000 * round(altitude.feet / 1000)))
return max(
altitude_for_lowest_speed,
min(altitude_for_highest_speed, rounded_altitude),
)
# TODO: somehow make the upper and lower limit configurable
return self.preferred_altitude(10, 33, "patrol")
def preferred_patrol_speed(self, altitude: Distance) -> Speed:
"""Preferred true airspeed when patrolling"""
@@ -289,9 +301,51 @@ class AircraftType(UnitType[Type[FlyingType]]):
)
else:
# Slow like warbirds or helicopters
# Use whichever is slowest - mach 0.35 or 70% of max speed
logging.debug(f"{self.name} max_speed * 0.7 is {max_speed * 0.7}")
return min(Speed.from_mach(0.35, altitude), max_speed * 0.7)
# Use whichever is slowest - mach 0.35 or 50% of max speed
logging.debug(
f"{self.display_name} max_speed * 0.5 is {max_speed * 0.5}"
)
return min(Speed.from_mach(0.35, altitude), max_speed * 0.5)
@cached_property
def preferred_cruise_altitude(self) -> Distance:
if self.cruise_altitude:
return self.cruise_altitude
else:
# TODO: somehow make the upper and lower limit configurable
return self.preferred_altitude(20, 20, "cruise")
@cached_property
def preferred_combat_altitude(self) -> Distance:
if self.combat_altitude:
return self.combat_altitude
else:
# TODO: somehow make the upper and lower limit configurable
return self.preferred_altitude(20, 20, "combat")
def preferred_altitude(self, low: int, high: int, type: str) -> Distance:
# Estimate based on max speed.
# Aircraft with max speed 600 kph will prefer low
# Aircraft with max speed 2800 kph will prefer high
altitude_for_lowest_speed = feet(low * 1000)
altitude_for_highest_speed = feet(high * 1000)
lowest_speed = kph(600)
highest_speed = kph(2800)
factor = (self.max_speed - lowest_speed).kph / (
highest_speed - lowest_speed
).kph
altitude = (
altitude_for_lowest_speed
+ (altitude_for_highest_speed - altitude_for_lowest_speed) * factor
)
logging.debug(
f"Preferred {type} altitude for {self.dcs_unit_type.id}: {altitude.feet}"
)
rounded_altitude = feet(round(1000 * round(altitude.feet / 1000)))
return max(
altitude_for_lowest_speed,
min(altitude_for_highest_speed, rounded_altitude),
)
def alloc_flight_radio(self, radio_registry: RadioRegistry) -> RadioFrequency:
from game.radio.radios import ChannelInUseError, kHz
@@ -326,8 +380,18 @@ class AircraftType(UnitType[Type[FlyingType]]):
def channel_name(self, radio_id: int, channel_id: int) -> str:
return self.channel_namer.channel_name(radio_id, channel_id)
def iter_props(self) -> Iterator[UnitProperty[Any]]:
return UnitProperty.for_aircraft(self.dcs_unit_type)
@cached_property
def laser_code_prop_ids(self) -> set[str]:
laser_code_props: set[str] = set()
for laser_code_config in self.laser_code_configs:
laser_code_props.update(laser_code_config.iter_prop_ids())
return laser_code_props
def iter_props(self) -> Iterator[UnitPropertyDescription]:
yield from self.dcs_unit_type.properties.values()
def should_show_prop(self, prop_id: str) -> bool:
return prop_id not in self.laser_code_prop_ids
def capable_of(self, task: FlightType) -> bool:
return task in self.task_priorities
@@ -336,16 +400,24 @@ class AircraftType(UnitType[Type[FlyingType]]):
return self.task_priorities[task]
def __setstate__(self, state: dict[str, Any]) -> None:
# Save compat: the `name` field has been renamed `variant_id`.
if "name" in state:
state["variant_id"] = state.pop("name")
# Update any existing models with new data on load.
updated = AircraftType.named(state["name"])
updated = AircraftType.named(state["variant_id"])
state.update(updated.__dict__)
self.__dict__.update(state)
@staticmethod
def _migrator() -> Dict[str, str]:
return {"F-15E Strike Eagle (AI)": "F-15E Strike Eagle"}
@classmethod
def named(cls, name: str) -> AircraftType:
if not cls._loaded:
cls._load_all()
return cls._by_name[name]
return cls._by_name[cls._migrator().get(name, name)]
@classmethod
def for_dcs_type(cls, dcs_unit_type: Type[FlyingType]) -> Iterator[AircraftType]:
@@ -378,11 +450,11 @@ class AircraftType(UnitType[Type[FlyingType]]):
@staticmethod
def _set_props_overrides(
config: Dict[str, Any], aircraft: Type[FlyingType], data_path: Path
config: Dict[str, Any], aircraft: Type[FlyingType]
) -> None:
if aircraft.property_defaults is None:
logging.warning(
f"'{data_path.name}' attempted to set default prop that does not exist."
f"'{aircraft.id}' attempted to set default prop that does not exist."
)
else:
for k in config:
@@ -390,28 +462,25 @@ class AircraftType(UnitType[Type[FlyingType]]):
aircraft.property_defaults[k] = config[k]
else:
logging.warning(
f"'{data_path.name}' attempted to set default prop '{k}' that does not exist"
f"'{aircraft.id}' attempted to set default prop '{k}' that does not exist"
)
@classmethod
def _each_variant_of(cls, aircraft: Type[FlyingType]) -> Iterator[AircraftType]:
# Replace slashes with underscores because slashes are not allowed in filenames
aircraft_id = aircraft.id.replace("/", "_")
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")
return
with data_path.open(encoding="utf-8") as data_file:
data = yaml.safe_load(data_file)
def _data_directory(cls) -> Path:
return Path("resources/units/aircraft")
@classmethod
def _variant_from_dict(
cls, aircraft: Type[FlyingType], variant_id: str, data: dict[str, Any]
) -> AircraftType:
try:
price = data["price"]
except KeyError as ex:
raise KeyError(f"Missing required price field: {data_path}") from ex
raise KeyError(f"Missing required price field") from ex
radio_config = RadioConfig.from_data(data.get("radios", {}))
patrol_config = PatrolConfig.from_data(data.get("patrol", {}))
altitudes_config = AltitudesConfig.from_data(data.get("altitudes", {}))
try:
mission_range = nautical_miles(int(data["max_range"]))
@@ -420,7 +489,7 @@ class AircraftType(UnitType[Type[FlyingType]]):
nautical_miles(50) if aircraft.helicopter else nautical_miles(150)
)
logging.warning(
f"{aircraft_id} does not specify a max_range. Defaulting to "
f"{variant_id} does not specify a max_range. Defaulting to "
f"{mission_range.nautical_miles}NM"
)
@@ -451,7 +520,7 @@ class AircraftType(UnitType[Type[FlyingType]]):
prop_overrides = data.get("default_overrides")
if prop_overrides is not None:
cls._set_props_overrides(prop_overrides, aircraft, data_path)
cls._set_props_overrides(prop_overrides, aircraft)
from game.ato.flighttype import FlightType
@@ -459,43 +528,77 @@ class AircraftType(UnitType[Type[FlyingType]]):
for task_name, priority in data.get("tasks", {}).items():
task_priorities[FlightType(task_name)] = priority
if FlightType.SEAD in task_priorities:
task_priorities[FlightType.SEAD_SWEEP] = task_priorities[FlightType.SEAD]
display_name = data.get("display_name", variant_id)
return AircraftType(
dcs_unit_type=aircraft,
variant_id=variant_id,
display_name=display_name,
description=data.get(
"description",
f"No data. <a href=\"https://google.com/search?q=DCS+{display_name.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {display_name}</span></a>",
),
year_introduced=introduction,
country_of_origin=data.get("origin", "No data."),
manufacturer=data.get("manufacturer", "No data."),
role=data.get("role", "No data."),
price=price,
carrier_capable=data.get("carrier_capable", False),
lha_capable=data.get("lha_capable", False),
always_keeps_gun=data.get("always_keeps_gun", False),
gunfighter=data.get("gunfighter", False),
max_group_size=data.get("max_group_size", aircraft.group_size_max),
patrol_altitude=patrol_config.altitude,
patrol_speed=patrol_config.speed,
cruise_altitude=altitudes_config.cruise,
combat_altitude=altitudes_config.combat,
max_mission_range=mission_range,
fuel_consumption=fuel_consumption,
default_livery=data.get("default_livery"),
intra_flight_radio=radio_config.intra_flight,
channel_allocator=radio_config.channel_allocator,
channel_namer=radio_config.channel_namer,
kneeboard_units=units,
utc_kneeboard=data.get("utc_kneeboard", False),
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),
laser_code_configs=[
LaserCodeConfig.from_yaml(d) for d in data.get("laser_codes", [])
],
use_f15e_waypoint_names=data.get("use_f15e_waypoint_names", False),
)
for variant in data.get("variants", [aircraft.id]):
yield AircraftType(
dcs_unit_type=aircraft,
name=variant,
description=data.get(
"description",
f"No data. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>",
),
year_introduced=introduction,
country_of_origin=data.get("origin", "No data."),
manufacturer=data.get("manufacturer", "No data."),
role=data.get("role", "No data."),
price=price,
carrier_capable=data.get("carrier_capable", False),
lha_capable=data.get("lha_capable", False),
always_keeps_gun=data.get("always_keeps_gun", False),
gunfighter=data.get("gunfighter", False),
max_group_size=data.get("max_group_size", aircraft.group_size_max),
patrol_altitude=patrol_config.altitude,
patrol_speed=patrol_config.speed,
max_mission_range=mission_range,
fuel_consumption=fuel_consumption,
default_livery=data.get("default_livery"),
intra_flight_radio=radio_config.intra_flight,
channel_allocator=radio_config.channel_allocator,
channel_namer=radio_config.channel_namer,
kneeboard_units=units,
utc_kneeboard=data.get("utc_kneeboard", False),
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),
has_built_in_target_pod=data.get("has_built_in_target_pod", False),
task_priorities=task_priorities,
)
@staticmethod
def _custom_weapon_injections(
aircraft: Type[FlyingType], data: Dict[str, Any]
) -> None:
if (wpn_injection := data.get("weapon_injections")) is not None:
pylons = [
v
for v in aircraft.__dict__.values()
if inspect.isclass(v) and v.__name__.startswith(f"Pylon")
]
pylons.sort(key=lambda x: int(x.__name__.replace("Pylon", "")))
for pylon_number, weapons in wpn_injection.items():
for w in weapons:
weapon = weapon_ids[w]
pylon = [
pylon
for pylon in pylons
if int(pylon.__name__.replace("Pylon", "")) == pylon_number
][0]
setattr(pylon, w, (pylon_number, weapon))
@staticmethod
def _user_weapon_injections(aircraft: Type[FlyingType]) -> None:
data_path = user_custom_weapon_injections_dir() / f"{aircraft.id}.yaml"
if not data_path.exists():
return
with data_path.open(encoding="utf-8") as data_file:
data = yaml.safe_load(data_file)
AircraftType._custom_weapon_injections(aircraft, data)
def __hash__(self) -> int:
return hash(self.name)
return hash(self.variant_id)

View File

@@ -6,7 +6,6 @@ from dataclasses import dataclass
from pathlib import Path
from typing import Any, ClassVar, Iterator, Optional, Type
import yaml
from dcs.unittype import VehicleType
from dcs.vehicles import vehicle_map
@@ -65,9 +64,29 @@ class GroundUnitType(UnitType[Type[VehicleType]]):
dict[type[VehicleType], list[GroundUnitType]]
] = defaultdict(list)
def __setstate__(self, state: dict[str, Any]) -> None:
# Save compat: the `name` field has been renamed `variant_id`.
if "name" in state:
state["variant_id"] = state.pop("name")
# iron-dome migration to IDF assets
if state["variant_id"] in [
"(IDF Mods Project) BM-21 Grad 122mm",
"(IDF Mods Project) Urgan BM-27 220mm",
"(IDF Mods Project) 9A52 Smerch CM 300mm",
]:
state["variant_id"] = "M109A6 Paladin"
elif state["variant_id"] == "Iron Dome ELM-2048 MMR":
state["variant_id"] = "ELM-2084MMR AD Rotating Mode"
# Update any existing models with new data on load.
updated = GroundUnitType.named(state["variant_id"])
state.update(updated.__dict__)
self.__dict__.update(state)
@classmethod
def register(cls, unit_type: GroundUnitType) -> None:
cls._by_name[unit_type.name] = unit_type
cls._by_name[unit_type.variant_id] = unit_type
cls._by_unit_type[unit_type.dcs_unit_type].append(unit_type)
@classmethod
@@ -87,15 +106,13 @@ class GroundUnitType(UnitType[Type[VehicleType]]):
yield from vehicle_map.values()
@classmethod
def _each_variant_of(cls, vehicle: Type[VehicleType]) -> Iterator[GroundUnitType]:
data_path = Path("resources/units/ground_units") / f"{vehicle.id}.yaml"
if not data_path.exists():
logging.warning(f"No data for {vehicle.id}; it will not be available")
return
with data_path.open(encoding="utf-8") as data_file:
data = yaml.safe_load(data_file)
def _data_directory(cls) -> Path:
return Path("resources/units/ground_units")
@classmethod
def _variant_from_dict(
cls, vehicle: Type[VehicleType], variant_id: str, data: dict[str, Any]
) -> GroundUnitType:
try:
introduction = data["introduced"]
if introduction is None:
@@ -110,23 +127,24 @@ class GroundUnitType(UnitType[Type[VehicleType]]):
else:
unit_class = UnitClass(class_name)
for variant in data.get("variants", [vehicle.id]):
yield GroundUnitType(
dcs_unit_type=vehicle,
unit_class=unit_class,
spawn_weight=data.get("spawn_weight", 0),
name=variant,
description=data.get(
"description",
f"No data. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>",
),
year_introduced=introduction,
country_of_origin=data.get("origin", "No data."),
manufacturer=data.get("manufacturer", "No data."),
role=data.get("role", "No data."),
price=data.get("price", 1),
skynet_properties=SkynetProperties.from_data(
data.get("skynet_properties", {})
),
reversed_heading=data.get("reversed_heading", False),
)
display_name = data.get("display_name", variant_id)
return GroundUnitType(
dcs_unit_type=vehicle,
unit_class=unit_class,
spawn_weight=data.get("spawn_weight", 0),
variant_id=variant_id,
display_name=display_name,
description=data.get(
"description",
f"No data. <a href=\"https://google.com/search?q=DCS+{display_name.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {display_name}</span></a>",
),
year_introduced=introduction,
country_of_origin=data.get("origin", "No data."),
manufacturer=data.get("manufacturer", "No data."),
role=data.get("role", "No data."),
price=data.get("price", 1),
skynet_properties=SkynetProperties.from_data(
data.get("skynet_properties", {})
),
reversed_heading=data.get("reversed_heading", False),
)

View File

@@ -0,0 +1,51 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import Iterator
from typing import Any
class LaserCodeConfig(ABC):
@staticmethod
def from_yaml(data: dict[str, Any]) -> LaserCodeConfig:
if (property_def := data.get("property")) is not None:
return SinglePropertyLaserCodeConfig(
property_def["id"], int(property_def["digits"])
)
return MultiplePropertyLaserCodeConfig(
[(d["id"], d["digit"]) for d in data["properties"]]
)
@abstractmethod
def iter_prop_ids(self) -> Iterator[str]:
...
@abstractmethod
def property_dict_for_code(self, code: int) -> dict[str, int]:
...
class SinglePropertyLaserCodeConfig(LaserCodeConfig):
def __init__(self, property_id: str, digits: int) -> None:
self.property_id = property_id
self.digits = digits
def iter_prop_ids(self) -> Iterator[str]:
yield self.property_id
def property_dict_for_code(self, code: int) -> dict[str, int]:
return {self.property_id: code % 10**self.digits}
class MultiplePropertyLaserCodeConfig(LaserCodeConfig):
def __init__(self, property_digit_mappings: list[tuple[str, int]]) -> None:
self.property_digit_mappings = property_digit_mappings
def iter_prop_ids(self) -> Iterator[str]:
yield from (i for i, p in self.property_digit_mappings)
def property_dict_for_code(self, code: int) -> dict[str, int]:
d = {}
for prop_id, idx in self.property_digit_mappings:
d[prop_id] = code // 10**idx % 10
return d

View File

@@ -1,12 +1,10 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import ClassVar, Iterator, Type
from typing import ClassVar, Iterator, Type, Any
import yaml
from dcs.ships import ship_map
from dcs.unittype import ShipType
@@ -21,9 +19,19 @@ class ShipUnitType(UnitType[Type[ShipType]]):
list
)
def __setstate__(self, state: dict[str, Any]) -> None:
# Save compat: the `name` field has been renamed `variant_id`.
if "name" in state:
state["variant_id"] = state.pop("name")
# Update any existing models with new data on load.
updated = ShipUnitType.named(state["variant_id"])
state.update(updated.__dict__)
self.__dict__.update(state)
@classmethod
def register(cls, unit_type: ShipUnitType) -> None:
cls._by_name[unit_type.name] = unit_type
cls._by_name[unit_type.variant_id] = unit_type
cls._by_unit_type[unit_type.dcs_unit_type].append(unit_type)
@classmethod
@@ -43,15 +51,13 @@ class ShipUnitType(UnitType[Type[ShipType]]):
yield from ship_map.values()
@classmethod
def _each_variant_of(cls, ship: Type[ShipType]) -> Iterator[ShipUnitType]:
data_path = Path("resources/units/ships") / f"{ship.id}.yaml"
if not data_path.exists():
logging.warning(f"No data for {ship.id}; it will not be available")
return
with data_path.open(encoding="utf-8") as data_file:
data = yaml.safe_load(data_file)
def _data_directory(cls) -> Path:
return Path("resources/units/ships")
@classmethod
def _variant_from_dict(
cls, ship: Type[ShipType], variant_id: str, data: dict[str, Any]
) -> ShipUnitType:
try:
introduction = data["introduced"]
if introduction is None:
@@ -62,18 +68,19 @@ class ShipUnitType(UnitType[Type[ShipType]]):
class_name = data.get("class")
unit_class = UnitClass(class_name)
for variant in data.get("variants", [ship.id]):
yield ShipUnitType(
dcs_unit_type=ship,
unit_class=unit_class,
name=variant,
description=data.get(
"description",
f"No data. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>",
),
year_introduced=introduction,
country_of_origin=data.get("origin", "No data."),
manufacturer=data.get("manufacturer", "No data."),
role=data.get("role", "No data."),
price=data.get("price"),
)
display_name = data.get("display_name", variant_id)
return ShipUnitType(
dcs_unit_type=ship,
unit_class=unit_class,
variant_id=variant_id,
display_name=data.get("display_name", variant_id),
description=data.get(
"description",
f"No data. <a href=\"https://google.com/search?q=DCS+{display_name.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {display_name}</span></a>",
),
year_introduced=introduction,
country_of_origin=data.get("origin", "No data."),
manufacturer=data.get("manufacturer", "No data."),
role=data.get("role", "No data."),
price=data["price"],
)

View File

@@ -1,12 +1,15 @@
from __future__ import annotations
import logging
from abc import ABC
from dataclasses import dataclass
from functools import cached_property
from typing import ClassVar, Generic, Iterator, Type, TypeVar
from typing_extensions import Self
from pathlib import Path
from typing import ClassVar, Generic, Iterator, Type, TypeVar, Any
import yaml
from dcs.unittype import UnitType as DcsUnitType
from typing_extensions import Self
from game.data.units import UnitClass
@@ -16,7 +19,8 @@ DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=Type[DcsUnitType])
@dataclass(frozen=True)
class UnitType(ABC, Generic[DcsUnitTypeT]):
dcs_unit_type: DcsUnitTypeT
name: str
variant_id: str
display_name: str
description: str
year_introduced: str
country_of_origin: str
@@ -28,7 +32,7 @@ class UnitType(ABC, Generic[DcsUnitTypeT]):
_loaded: ClassVar[bool] = False
def __str__(self) -> str:
return self.name
return self.display_name
@property
def dcs_id(self) -> str:
@@ -50,8 +54,29 @@ class UnitType(ABC, Generic[DcsUnitTypeT]):
def each_dcs_type() -> Iterator[DcsUnitTypeT]:
raise NotImplementedError
@classmethod
def _data_directory(cls) -> Path:
raise NotImplementedError
@classmethod
def _each_variant_of(cls, unit: DcsUnitTypeT) -> Iterator[Self]:
data_path = cls._data_directory() / f"{unit.id}.yaml"
if not data_path.exists():
logging.warning(f"No data for {unit.id}; it will not be available")
return
with data_path.open(encoding="utf-8") as data_file:
data = yaml.safe_load(data_file)
for variant_id, variant_data in data.get("variants", {unit.id: {}}).items():
if variant_data is None:
variant_data = {}
yield cls._variant_from_dict(unit, variant_id, data | variant_data)
@classmethod
def _variant_from_dict(
cls, dcs_unit_type: DcsUnitTypeT, variant_id: str, data: dict[str, Any]
) -> Self:
raise NotImplementedError
@classmethod

View File

@@ -45,6 +45,9 @@ class Faction:
#: choose the default locale.
locales: Optional[List[str]]
# The unit type to spawn for cargo shipping.
cargo_ship: ShipUnitType
# Country used by this faction
country: Country
@@ -166,7 +169,7 @@ class Faction:
def air_defenses(self) -> list[str]:
"""Returns the Air Defense types"""
# This is used for the faction overview in NewGameWizard
air_defenses = [a.name for a in self.air_defense_units]
air_defenses = [a.variant_id for a in self.air_defense_units]
air_defenses.extend(
[
pg.name
@@ -177,7 +180,7 @@ class Faction:
return sorted(air_defenses)
@cached_property
def aircrafts(self) -> list[UnitType[Any]]:
def all_aircrafts(self) -> list[UnitType[Any]]:
# Migrator can't cope with this, so we need to do it here...
self.aircraft = set(self.aircraft)
self.awacs = set(self.awacs)
@@ -194,7 +197,11 @@ class Faction:
"country ID"
) from ex
faction = Faction(locales=json.get("locales"), country=country)
faction = Faction(
locales=json.get("locales"),
country=country,
cargo_ship=ShipUnitType.named(json.get("cargo_ship", "Bulker Handy Wind")),
)
faction.name = json.get("name", "")
if not faction.name:
@@ -328,9 +335,13 @@ class Faction:
if not mod_settings.uh_60l:
self.remove_aircraft("UH-60L")
self.remove_aircraft("KC130J")
if not mod_settings.fa18ef_tanker:
self.remove_aircraft("Superbug_AITanker")
if not mod_settings.f4bc_phantom:
self.remove_aircraft("VSN_F4B")
self.remove_aircraft("VSN_F4C")
if not mod_settings.f9f_panther:
self.remove_aircraft("VSN_F9F")
if not mod_settings.f15d_baz:
self.remove_aircraft("F-15D")
if not mod_settings.f_15_idf:
@@ -372,6 +383,8 @@ class Faction:
self.remove_aircraft("JAS39Gripen")
self.remove_aircraft("JAS39Gripen_BVR")
self.remove_aircraft("JAS39Gripen_AG")
if not mod_settings.super_etendard:
self.remove_aircraft("VSN_SEM")
if not mod_settings.su30_flanker_h:
self.remove_aircraft("Su-30MKA")
self.remove_aircraft("Su-30MKI")
@@ -449,13 +462,16 @@ class Faction:
self.remove_ship("L02")
self.remove_ship("DDG39")
if not mod_settings.irondome:
self.remove_vehicle("I9K51_GRAD")
self.remove_vehicle("I9K57_URAGAN")
self.remove_vehicle("I9K58_SMERCH")
self.remove_vehicle("IRON_DOME_CP")
self.remove_vehicle("Iron_Dome_David_Sling_CP")
self.remove_vehicle("IRON_DOME_LN")
self.remove_vehicle("ELM2048_MMR")
self.remove_vehicle("DAVID_SLING_LN")
self.remove_vehicle("ELM2084_MMR_AD_RT")
self.remove_vehicle("ELM2084_MMR_AD_SC")
self.remove_vehicle("ELM2084_MMR_WLR")
self.remove_preset("Iron Dome")
self.remove_preset("Iron Dome (Semicircle)")
self.remove_preset("David's Sling")
self.remove_preset("David's Sling (Semicircle)")
# swedish military assets pack
if not mod_settings.swedishmilitaryassetspack:
self.remove_vehicle("BV410_RBS70")
@@ -495,6 +511,10 @@ class Faction:
self.remove_ship("HSwMS_Visby")
self.remove_ship("Strb90")
self.remove_aircraft("HKP15B")
self.remove_preset("LvS-103 Rb103A")
self.remove_preset("LvS-103 Rb103A Mobile")
self.remove_preset("LvS-103 Rb103B")
self.remove_preset("LvS-103 Rb103B Mobile")
# SWPack
if not mod_settings.SWPack:
self.remove_aircraft("AWINGA")
@@ -528,14 +548,16 @@ class Faction:
self.remove_ship("Destroyer_carrier")
def remove_aircraft(self, name: str) -> None:
for i in list(self.aircrafts):
if i.dcs_unit_type.id == name:
self.aircrafts.remove(i)
for aircraft_set in [self.aircraft, self.awacs, self.tankers]:
for i in list(aircraft_set):
if i.dcs_unit_type.id == name:
aircraft_set.remove(i)
def remove_aircraft_by_name(self, name: str) -> None:
for i in list(self.aircrafts):
if i.name == name:
self.aircrafts.remove(i)
for aircraft_set in [self.aircraft, self.awacs, self.tankers]:
for i in list(aircraft_set):
if i.display_name == name:
aircraft_set.remove(i)
def remove_preset(self, name: str) -> None:
for pg in self.preset_groups:

View File

@@ -1,3 +1,2 @@
from .holdzonegeometry import HoldZoneGeometry
from .ipzonegeometry import IpZoneGeometry
from .joinzonegeometry import JoinZoneGeometry

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