Merge remote-tracking branch 'remotes/dcs-retribution/dcs-retribution/dev' into pretense-generator

This commit is contained in:
MetalStormGhost 2024-05-03 11:19:48 +03:00
commit c795ed01a0
151 changed files with 13076 additions and 6698 deletions

View File

@ -31,9 +31,8 @@ body:
If the bug was found in a test/development build, select "Test build" or "Development build" 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. and provide a link to the applicable build in the field below.
options: options:
- v1.2.1 - v1.3.1
- Test build - Test build
- Development build
- type: input - type: input
attributes: attributes:
label: Test/Development build label: Test/Development build

View File

@ -39,9 +39,8 @@ body:
If the bug was found in a test/development build, select "Test build" or "Development build" 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. and provide a link to the applicable build in the field below.
options: options:
- 1.0.0 - 1.3.1
- Test build - Test build
- Development build
- type: input - type: input
attributes: attributes:
label: Test/Development build label: Test/Development build

View File

@ -6,7 +6,7 @@ runs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: "3.10" python-version: "3.11"
cache: pip cache: pip
- name: Install environment - name: Install environment

View File

@ -49,7 +49,7 @@ jobs:
include_image: true include_image: true
avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
username: GitHub Build Notifier username: GitHub Build Notifier
if: startsWith(github.ref_name, 'test') if: github.repository == 'dcs-retribution/dcs-retribution' && startsWith(github.ref_name, 'test')
- name: Send status to Discord - name: Send status to Discord
uses: stegzilla/discord-notify@v2 uses: stegzilla/discord-notify@v2
@ -60,4 +60,4 @@ jobs:
include_image: true include_image: true
avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
username: GitHub Build Notifier username: GitHub Build Notifier
if: github.ref_name == 'dev' if: github.repository == 'dcs-retribution/dcs-retribution' && github.ref_name == 'dev'

View File

@ -2,7 +2,7 @@
(Github Readme Banner and Splash screen Artwork by Andriy Dankovych, CC BY-SA 4.0) (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) [![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,30 @@
# Retribution v1.4.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
* **[Payload Editor]** Ability to configure liveries on flight/flight-member level
* **[Factions]** Support for definitions in yml/yaml format
* **[Campaigns/Factions]** Support for inline recommended faction in campaign's yaml file
* **[Squadrons]** Ability to define a livery-set for each squadron from which Retribution will randomly choose during mission generation
* **[Modding]** Updated support for F/A-18E/F/G mod version 2.2.5
* **[Campaign Setup]** Allow adjustments to naval TGOs (except carriers) on turn 0
* **[Campaign Design]** Ability to configure specific carrier names & types in campaign's yaml file
* **[Mission Generation]** Ability to inject custom kneeboards
## Fixes
* **[UI/UX]** A-10A flights can be edited again.
* **[Mission Generation]** IADS bug sometimes triggering "no skynet usable units" error during mission generation
* **[New Game Wizard]** Campaign errors show a dialog again and avoid CTDs
# Retribution v1.3.1
#### 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.
## Fixes
* **[UX]** Fix save-compatibility issue
* **[UX]** Avoid crash on startup due to incompatible save
# Retribution v1.3.0 # 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. #### 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.
@ -15,6 +42,8 @@
* **[Modding]** Updated support for F-4B/C Phantom mod to 2.8.7.204 * **[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]** 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 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 * **[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]** Added HF-FM band for AN/ARC-222
* **[Radios]** Ability to define preset channels for radios on squadron level (for human pilots only) * **[Radios]** Ability to define preset channels for radios on squadron level (for human pilots only)
@ -41,6 +70,12 @@
* **[Config]** Preference setting to use custom Liberation payloads instead of prioritizing Retribution's default * **[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 * **[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]** 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 ## Fixes
* **[Mission Generation]** Anti-ship strikes should use "group attack" in their attack-task * **[Mission Generation]** Anti-ship strikes should use "group attack" in their attack-task
@ -266,6 +301,7 @@ BAI/ANTISHIP/DEAD/STRIKE/BARCAP/CAS/OCA/AIR-ASSAULT (main) missions
* **[UI]** Fixed flight properties UI to support F-15E S4+ laser codes. * **[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 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]** 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 # 8.1.0

13169
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,26 +6,26 @@
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"homepage": ".", "homepage": ".",
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^1.8.5", "@reduxjs/toolkit": "^1.9.7",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3", "@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.1.2", "@types/jest": "^29.5.12",
"@types/node": "^18.8.3", "@types/node": "^18.19.26",
"@types/react": "^18.0.21", "@types/react": "^18.2.69",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.2.22",
"@types/react-redux": "^7.1.24", "@types/react-redux": "^7.1.33",
"axios": "^1.6.0", "axios": "^1.6.8",
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
"esri-leaflet": "^3.0.8", "esri-leaflet": "^3.0.12",
"leaflet": "^1.9.2", "leaflet": "^1.9.4",
"leaflet-ruler": "^1.0.0", "leaflet-ruler": "^1.0.0",
"milsymbol": "^2.0.0", "milsymbol": "^2.2.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-esri-leaflet": "^2.0.1", "react-esri-leaflet": "^2.0.1",
"react-leaflet": "^4.1.0", "react-leaflet": "^4.2.1",
"react-redux": "^8.0.4", "react-redux": "^8.1.3",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"typescript": "~4.8.4" "typescript": "~4.8.4"
}, },
@ -61,20 +61,20 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@rtk-query/codegen-openapi": "^1.0.0", "@rtk-query/codegen-openapi": "^1.2.0",
"@trivago/prettier-plugin-sort-imports": "^4.3.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/leaflet": "^1.8.0", "@types/leaflet": "^1.9.8",
"@types/redux-logger": "^3.0.9", "@types/redux-logger": "^3.0.13",
"@types/websocket": "^1.0.5", "@types/websocket": "^1.0.10",
"electron": "^22.3.25", "electron": "^22.3.27",
"electron-is-dev": "^2.0.0", "electron-is-dev": "^2.0.0",
"generate-license-file": "^2.0.0", "generate-license-file": "^2.0.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"license-checker": "^25.0.1", "license-checker": "^25.0.1",
"nth-check": "^2.0.1", "nth-check": "^2.1.1",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.2",
"wait-on": "^6.0.1" "wait-on": "^7.2.0"
}, },
"jest": { "jest": {
"transformIgnorePatterns": [ "transformIgnorePatterns": [

View File

@ -1,10 +1,10 @@
import { Flight } from "../../api/liberationApi"; import { Flight } from "../../api/liberationApi";
import { Icon, Point } from "leaflet"; import { Icon, Point } from "leaflet";
import { Symbol } from "milsymbol"; import ms from "milsymbol";
import { Marker } from "react-leaflet"; import { Marker } from "react-leaflet";
function iconForFlight(flight: Flight) { function iconForFlight(flight: Flight) {
const symbol = new Symbol(flight.sidc, { const symbol = new ms.Symbol(flight.sidc, {
size: 20, size: 20,
}); });

View File

@ -1,9 +1,9 @@
import { ControlPoint } from "../../api/_liberationApi"; import { ControlPoint } from "../../api/_liberationApi";
import { Icon, Point } from "leaflet"; import { Icon, Point } from "leaflet";
import { Symbol } from "milsymbol"; import ms from "milsymbol";
export const iconForControlPoint = (cp: ControlPoint) => { export const iconForControlPoint = (cp: ControlPoint) => {
const symbol = new Symbol(cp.sidc, { const symbol = new ms.Symbol(cp.sidc, {
size: 24, size: 24,
colorMode: "Dark", colorMode: "Dark",
}); });

View File

@ -1,7 +1,8 @@
import { Flight } from "../../api/liberationApi"; import { Flight } from "../../api/liberationApi";
import { useGetCommitBoundaryForFlightQuery } from "../../api/liberationApi"; import { useGetCommitBoundaryForFlightQuery } from "../../api/liberationApi";
import WaypointMarker from "../waypointmarker"; 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"; import { Polyline } from "react-leaflet";
const BLUE_PATH = "#0084ff"; const BLUE_PATH = "#0084ff";
@ -27,16 +28,44 @@ const pathColor = (props: FlightPlanProps) => {
function FlightPlanPath(props: FlightPlanProps) { function FlightPlanPath(props: FlightPlanProps) {
const color = pathColor(props); const color = pathColor(props);
const waypoints = props.flight.waypoints; 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) { if (waypoints == null) {
return <></>; return <></>;
} }
const points = waypoints const points = waypoints
.filter((waypoint) => waypoint.include_in_path) .filter((waypoint) => waypoint.include_in_path)
.map((waypoint) => waypoint.position); .map((waypoint) => waypoint.position);
return ( return (
<Polyline <Polyline
positions={points} positions={points}
pathOptions={{ color: color, interactive: false }} 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", () => { it("are not drawn if wrong coalition", () => {
renderWithProviders(<FlightPlansLayer blue={true} />, { renderWithProviders(<FlightPlansLayer blue={true} />, {

View File

@ -5,11 +5,11 @@ import {
import { Tgo as TgoModel } from "../../api/liberationApi"; import { Tgo as TgoModel } from "../../api/liberationApi";
import SplitLines from "../splitlines/SplitLines"; import SplitLines from "../splitlines/SplitLines";
import { Icon, Point } from "leaflet"; import { Icon, Point } from "leaflet";
import { Symbol as MilSymbol } from "milsymbol"; import ms from "milsymbol";
import { Marker, Tooltip } from "react-leaflet"; import { Marker, Tooltip } from "react-leaflet";
function iconForTgo(cp: TgoModel) { function iconForTgo(cp: TgoModel) {
const symbol = new MilSymbol(cp.sidc, { const symbol = new ms.Symbol(cp.sidc, {
size: 24, size: 24,
}); });

View File

@ -6,10 +6,10 @@
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "DCS Liberation" project = "DCS Retribution"
copyright = "2023, DCS Liberation Team" copyright = "2024, DCS Retribution Team"
author = "DCS Liberation Team" author = "DCS Retribution Team"
release = "7.0.0" release = "1.4.0"
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import random
import uuid import uuid
from collections.abc import Iterator from collections.abc import Iterator
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -9,6 +10,7 @@ from dcs import Point
from dcs.planes import C_101CC, C_101EB, Su_33, FA_18C_hornet from dcs.planes import C_101CC, C_101EB, Su_33, FA_18C_hornet
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
from game.theater import ControlPoint, MissionTarget
from pydcs_extensions.hercules.hercules import Hercules from pydcs_extensions.hercules.hercules import Hercules
from .flightmembers import FlightMembers from .flightmembers import FlightMembers
from .flightroster import FlightRoster from .flightroster import FlightRoster
@ -33,7 +35,6 @@ if TYPE_CHECKING:
from game.sim.gameupdateevents import GameUpdateEvents from game.sim.gameupdateevents import GameUpdateEvents
from game.sim.simulationresults import SimulationResults from game.sim.simulationresults import SimulationResults
from game.squadrons import Squadron, Pilot from game.squadrons import Squadron, Pilot
from game.theater import ControlPoint
from game.transfers import TransferOrder from game.transfers import TransferOrder
from game.data.weapons import WeaponType from game.data.weapons import WeaponType
from .flightmember import FlightMember from .flightmember import FlightMember
@ -88,6 +89,7 @@ class Flight(
self.initialize_fuel() self.initialize_fuel()
self.use_same_loadout_for_all_members = True self.use_same_loadout_for_all_members = True
self.use_same_livery_for_all_members = True
# Only used by transport missions. # Only used by transport missions.
self.cargo = cargo self.cargo = cargo
@ -123,6 +125,11 @@ class Flight(
) )
) )
# 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 @property
def available_callsigns(self) -> List[str]: def available_callsigns(self) -> List[str]:
callsigns = set() callsigns = set()
@ -218,6 +225,13 @@ class Flight(
def points(self) -> List[FlightWaypoint]: def points(self) -> List[FlightWaypoint]:
return self.flight_plan.waypoints[1:] 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: def position(self) -> Point:
return self.state.estimate_position() return self.state.estimate_position()

View File

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any, Optional
from game.ato.loadouts import Loadout from game.ato.loadouts import Loadout
from game.lasercodes import LaserCode from game.lasercodes import LaserCode
@ -17,6 +17,7 @@ class FlightMember:
self.tgp_laser_code: LaserCode | None = None self.tgp_laser_code: LaserCode | None = None
self.weapon_laser_code: LaserCode | None = None self.weapon_laser_code: LaserCode | None = None
self.properties: dict[str, bool | float | int] = {} self.properties: dict[str, bool | float | int] = {}
self.livery: Optional[str] = None
def __setstate__(self, state: dict[str, Any]) -> None: def __setstate__(self, state: dict[str, Any]) -> None:
if "tgp_laser_code" not in state: if "tgp_laser_code" not in state:

View File

@ -95,6 +95,13 @@ class FlightMembers(IFlightRoster):
# across all flight members. # across all flight members.
member.loadout = loadout member.loadout = loadout
def use_same_livery_for_all_members(self) -> None:
if not self.members:
return
livery = self.members[0].livery
for member in self.members[1:]:
member.livery = livery
def use_distinct_loadouts_for_each_member(self) -> None: def use_distinct_loadouts_for_each_member(self) -> None:
for member in self.members: for member in self.members:
member.loadout = member.loadout.clone() 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.ibuilder import IBuilder
from game.ato.flightplans.patrolling import PatrollingFlightPlan, PatrollingLayout from game.ato.flightplans.patrolling import PatrollingFlightPlan, PatrollingLayout
from game.ato.flightplans.waypointbuilder import WaypointBuilder 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]): class AewcFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
@ -70,10 +70,7 @@ class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]):
builder = WaypointBuilder(self.flight) builder = WaypointBuilder(self.flight)
if self.flight.unit_type.patrol_altitude is not None: altitude = builder.get_patrol_altitude
altitude = self.flight.unit_type.patrol_altitude
else:
altitude = feet(25000)
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude) racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
@ -90,6 +87,7 @@ class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]):
arrival=builder.land(self.flight.arrival), arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert), divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
custom_waypoints=list(),
) )
def build(self, dump_debug_info: bool = False) -> AewcFlightPlan: def build(self, dump_debug_info: bool = False) -> AewcFlightPlan:

View File

@ -49,6 +49,7 @@ class AirAssaultLayout(FormationAttackLayout):
if self.divert is not None: if self.divert is not None:
yield self.divert yield self.divert
yield self.bullseye yield self.bullseye
yield from self.custom_waypoints
class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay): class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
@ -111,12 +112,11 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
) )
assert self.package.waypoints is not None assert self.package.waypoints is not None
heli_alt = feet(self.coalition.game.settings.heli_cruise_alt_agl)
altitude = heli_alt 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)
altitude = builder.get_cruise_altitude
altitude_is_agl = self.flight.is_helo
if self.flight.is_hercules or self.flight.departure.cptype in [ if self.flight.is_hercules or self.flight.departure.cptype in [
ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.AIRCRAFT_CARRIER_GROUP,
ControlPointType.LHA_GROUP, ControlPointType.LHA_GROUP,
@ -133,14 +133,22 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
self._generate_ctld_pickup(), self._generate_ctld_pickup(),
) )
) )
pickup.alt = heli_alt pickup.alt = altitude
pickup_position = pickup.position pickup_position = pickup.position
ingress = builder.ingress( ingress = (
builder.ingress(
FlightWaypointType.INGRESS_AIR_ASSAULT, FlightWaypointType.INGRESS_AIR_ASSAULT,
self.package.waypoints.ingress, self.package.waypoints.ingress,
self.package.target, self.package.target,
) )
if not self.flight.is_hercules
else builder.ingress(
FlightWaypointType.INGRESS_AIR_ASSAULT,
self.package.waypoints.initial,
self.package.target,
)
)
assault_area = builder.assault_area(self.package.target) assault_area = builder.assault_area(self.package.target)
if self.flight.is_hercules: if self.flight.is_hercules:
@ -159,8 +167,6 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
drop_pos = tgt.position.point_from_heading(heading, 1200) drop_pos = tgt.position.point_from_heading(heading, 1200)
drop_off_zone = MissionTarget("Dropoff zone", drop_pos) drop_off_zone = MissionTarget("Dropoff zone", drop_pos)
dz = builder.dropoff_zone(drop_off_zone) if self.flight.is_helo else None dz = builder.dropoff_zone(drop_off_zone) if self.flight.is_helo else None
if dz:
dz.alt = heli_alt
return AirAssaultLayout( return AirAssaultLayout(
departure=builder.takeoff(self.flight.departure), departure=builder.takeoff(self.flight.departure),
@ -184,9 +190,10 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
divert=builder.divert(self.flight.divert), divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
hold=None, hold=None,
join=builder.join(ingress.position), join=builder.join(self.package.waypoints.ingress),
split=builder.split(self.flight.arrival.position), split=builder.split(self.flight.arrival.position),
refuel=None, refuel=None,
custom_waypoints=list(),
) )
def build(self, dump_debug_info: bool = False) -> AirAssaultFlightPlan: def build(self, dump_debug_info: bool = False) -> AirAssaultFlightPlan:

View File

@ -8,7 +8,7 @@ from typing import Optional
from typing import TYPE_CHECKING, Type from typing import TYPE_CHECKING, Type
from game.theater.missiontarget import MissionTarget from game.theater.missiontarget import MissionTarget
from game.utils import feet, Distance from game.utils import Distance
from ._common_ctld import generate_random_ctld_point from ._common_ctld import generate_random_ctld_point
from .ibuilder import IBuilder from .ibuilder import IBuilder
from .planningerror import PlanningError from .planningerror import PlanningError
@ -92,6 +92,7 @@ class AirliftLayout(StandardLayout):
if self.divert is not None: if self.divert is not None:
yield self.divert yield self.divert
yield self.bullseye yield self.bullseye
yield from self.custom_waypoints
class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]): class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
@ -132,12 +133,11 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
"Cannot plan transport mission for flight with no cargo." "Cannot plan transport mission for flight with no cargo."
) )
heli_alt = feet(self.coalition.game.settings.heli_cruise_alt_agl)
altitude = heli_alt 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)
altitude = builder.get_cruise_altitude
altitude_is_agl = self.flight.is_helo
pickup_ascent = None pickup_ascent = None
pickup_descent = None pickup_descent = None
pickup = None pickup = None
@ -246,6 +246,7 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
arrival=builder.land(self.flight.arrival), arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert), divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
custom_waypoints=list(),
) )
def build(self, dump_debug_info: bool = False) -> AirliftFlightPlan: def build(self, dump_debug_info: bool = False) -> AirliftFlightPlan:

View File

@ -1,11 +1,10 @@
from __future__ import annotations from __future__ import annotations
import random
from datetime import timedelta from datetime import timedelta
from typing import Type from typing import Type
from game.theater import FrontLine from game.theater import FrontLine
from game.utils import Distance, Speed, feet from game.utils import Distance, Speed
from .capbuilder import CapBuilder from .capbuilder import CapBuilder
from .invalidobjectivelocation import InvalidObjectiveLocation from .invalidobjectivelocation import InvalidObjectiveLocation
from .patrolling import PatrollingFlightPlan, PatrollingLayout 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) 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) builder = WaypointBuilder(self.flight)
patrol_alt = builder.get_patrol_altitude
start, end = builder.race_track(start_pos, end_pos, patrol_alt) start, end = builder.race_track(start_pos, end_pos, patrol_alt)
return PatrollingLayout( return PatrollingLayout(
@ -64,6 +58,7 @@ class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
arrival=builder.land(self.flight.arrival), arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert), divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
custom_waypoints=list(),
) )
def build(self, dump_debug_info: bool = False) -> BarCapFlightPlan: def build(self, dump_debug_info: bool = False) -> BarCapFlightPlan:

View File

@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Type
from game.theater import FrontLine from game.theater import FrontLine
from game.utils import Distance, Speed, kph, dcs_to_shapely_point from game.utils import Distance, Speed, kph, dcs_to_shapely_point
from game.utils import feet, nautical_miles from game.utils import nautical_miles
from .ibuilder import IBuilder from .ibuilder import IBuilder
from .invalidobjectivelocation import InvalidObjectiveLocation from .invalidobjectivelocation import InvalidObjectiveLocation
from .patrolling import PatrollingFlightPlan, PatrollingLayout from .patrolling import PatrollingFlightPlan, PatrollingLayout
@ -37,6 +37,7 @@ class CasLayout(PatrollingLayout):
if self.divert is not None: if self.divert is not None:
yield self.divert yield self.divert
yield self.bullseye yield self.bullseye
yield from self.custom_waypoints
class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay): class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay):
@ -104,11 +105,7 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]):
builder = WaypointBuilder(self.flight) builder = WaypointBuilder(self.flight)
is_helo = self.flight.unit_type.dcs_unit_type.helicopter is_helo = self.flight.unit_type.dcs_unit_type.helicopter
ingress_egress_altitude = ( ingress_egress_altitude = builder.get_combat_altitude
self.doctrine.ingress_altitude
if not is_helo
else feet(self.coalition.game.settings.heli_combat_alt_agl)
)
use_agl_patrol_altitude = is_helo use_agl_patrol_altitude = is_helo
ip_solver = IpSolver( ip_solver = IpSolver(
@ -167,6 +164,7 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]):
arrival=builder.land(self.flight.arrival), arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert), divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
custom_waypoints=list(),
) )
def build(self, dump_debug_info: bool = False) -> CasFlightPlan: def build(self, dump_debug_info: bool = False) -> CasFlightPlan:

View File

@ -17,8 +17,6 @@ if TYPE_CHECKING:
@dataclass @dataclass
class CustomLayout(Layout): class CustomLayout(Layout):
custom_waypoints: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]: def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure yield self.departure
yield from self.custom_waypoints yield from self.custom_waypoints

View File

@ -11,7 +11,6 @@ from .formationattack import (
) )
from .waypointbuilder import WaypointBuilder from .waypointbuilder import WaypointBuilder
from .. import FlightType from .. import FlightType
from ...utils import feet
class EscortFlightPlan(FormationAttackFlightPlan): class EscortFlightPlan(FormationAttackFlightPlan):
@ -43,12 +42,9 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
split = builder.split(self._get_split()) split = builder.split(self._get_split())
ingress_alt = self.doctrine.ingress_altitude
is_helo = builder.flight.is_helo is_helo = builder.flight.is_helo
heli_alt = feet(self.coalition.game.settings.heli_combat_alt_agl)
initial = builder.escort_hold( initial = builder.escort_hold(
target.position if is_helo else self.package.waypoints.initial, target.position if is_helo else self.package.waypoints.initial,
min(heli_alt, ingress_alt) if is_helo else ingress_alt,
) )
pf = self.package.primary_flight pf = self.package.primary_flight
@ -69,9 +65,6 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
if layout.drop_off: if layout.drop_off:
initial = builder.escort_hold( initial = builder.escort_hold(
layout.drop_off.position, layout.drop_off.position,
min(feet(200), ingress_alt)
if builder.flight.is_helo
else ingress_alt,
) )
refuel = self._build_refuel(builder) refuel = self._build_refuel(builder)
@ -80,13 +73,13 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
nav_to = builder.nav_path( nav_to = builder.nav_path(
hold.position if hold else departure.position, hold.position if hold else departure.position,
join.position, join.position,
self.doctrine.ingress_altitude, builder.get_cruise_altitude,
) )
nav_from = builder.nav_path( nav_from = builder.nav_path(
refuel.position if refuel else split.position, refuel.position if refuel else split.position,
self.flight.arrival.position, self.flight.arrival.position,
self.doctrine.ingress_altitude, builder.get_cruise_altitude,
) )
return FormationAttackLayout( return FormationAttackLayout(
@ -103,6 +96,7 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
arrival=builder.land(self.flight.arrival), arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert), divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
custom_waypoints=list(),
) )
def build(self, dump_debug_info: bool = False) -> EscortFlightPlan: def build(self, dump_debug_info: bool = False) -> EscortFlightPlan:

View File

@ -24,6 +24,7 @@ class FerryLayout(StandardLayout):
if self.divert is not None: if self.divert is not None:
yield self.divert yield self.divert
yield self.bullseye yield self.bullseye
yield from self.custom_waypoints
class FerryFlightPlan(StandardFlightPlan[FerryLayout]): class FerryFlightPlan(StandardFlightPlan[FerryLayout]):
@ -60,14 +61,14 @@ class Builder(IBuilder[FerryFlightPlan, FerryLayout]):
f"{self.flight.departure}" f"{self.flight.departure}"
) )
builder = WaypointBuilder(self.flight)
altitude_is_agl = self.flight.is_helo altitude_is_agl = self.flight.is_helo
altitude = ( altitude = (
feet(self.coalition.game.settings.heli_cruise_alt_agl) feet(self.coalition.game.settings.heli_cruise_alt_agl)
if altitude_is_agl if altitude_is_agl
else self.flight.unit_type.preferred_patrol_altitude else builder.get_patrol_altitude
) )
builder = WaypointBuilder(self.flight)
return FerryLayout( return FerryLayout(
departure=builder.takeoff(self.flight.departure), departure=builder.takeoff(self.flight.departure),
nav_to=builder.nav_path( nav_to=builder.nav_path(
@ -80,6 +81,7 @@ class Builder(IBuilder[FerryFlightPlan, FerryLayout]):
divert=builder.divert(self.flight.divert), divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
nav_from=[], nav_from=[],
custom_waypoints=list(),
) )
def build(self, dump_debug_info: bool = False) -> FerryFlightPlan: def build(self, dump_debug_info: bool = False) -> FerryFlightPlan:

View File

@ -34,6 +34,7 @@ if TYPE_CHECKING:
@dataclass @dataclass
class Layout(ABC): class Layout(ABC):
departure: FlightWaypoint departure: FlightWaypoint
custom_waypoints: list[FlightWaypoint]
@property @property
def waypoints(self) -> list[FlightWaypoint]: def waypoints(self) -> list[FlightWaypoint]:

View File

@ -11,7 +11,7 @@ from dcs import Point
from game.flightplan import HoldZoneGeometry from game.flightplan import HoldZoneGeometry
from game.theater import MissionTarget from game.theater import MissionTarget
from game.utils import Speed, meters, nautical_miles, feet from game.utils import Speed, meters, nautical_miles
from .flightplan import FlightPlan from .flightplan import FlightPlan
from .formation import FormationFlightPlan, FormationLayout from .formation import FormationFlightPlan, FormationLayout
from .ibuilder import IBuilder from .ibuilder import IBuilder
@ -157,6 +157,7 @@ class FormationAttackLayout(FormationLayout):
if self.divert is not None: if self.divert is not None:
yield self.divert yield self.divert
yield self.bullseye yield self.bullseye
yield from self.custom_waypoints
FlightPlanT = TypeVar("FlightPlanT", bound=FlightPlan[FormationAttackLayout]) FlightPlanT = TypeVar("FlightPlanT", bound=FlightPlan[FormationAttackLayout])
@ -209,14 +210,10 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
if self.flight.flight_type == FlightType.STRIKE: if self.flight.flight_type == FlightType.STRIKE:
hdg = self.package.target.position.heading_between_point(ingress.position) hdg = self.package.target.position.heading_between_point(ingress.position)
pos = ingress.position.point_from_heading(hdg, nautical_miles(10).meters) pos = ingress.position.point_from_heading(hdg, nautical_miles(10).meters)
lineup = builder.nav(pos, self.flight.coalition.doctrine.ingress_altitude) lineup = builder.nav(pos, builder.get_combat_altitude)
is_helo = self.flight.is_helo is_helo = self.flight.is_helo
ingress_egress_altitude = ( ingress_egress_altitude = builder.get_combat_altitude
self.doctrine.ingress_altitude
if not is_helo
else feet(self.coalition.game.settings.heli_combat_alt_agl)
)
use_agl_ingress_egress = is_helo use_agl_ingress_egress = is_helo
return FormationAttackLayout( return FormationAttackLayout(
@ -244,6 +241,7 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
arrival=builder.land(self.flight.arrival), arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert), divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
custom_waypoints=list(),
) )
def _build_refuel(self, builder: WaypointBuilder) -> Optional[FlightWaypoint]: def _build_refuel(self, builder: WaypointBuilder) -> Optional[FlightWaypoint]:

View File

@ -25,6 +25,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
def __init__(self, flight: Flight) -> None: def __init__(self, flight: Flight) -> None:
self.flight = flight self.flight = flight
self._flight_plan: FlightPlanT | None = None self._flight_plan: FlightPlanT | None = None
self.settings = self.flight.coalition.game.settings
def get_or_build(self) -> FlightPlanT: def get_or_build(self) -> FlightPlanT:
if self._flight_plan is None: if self._flight_plan is None:

View File

@ -5,7 +5,7 @@ from typing import Type
from dcs import Point from dcs import Point
from game.utils import Distance, Heading, feet, meters from game.utils import Distance, Heading, meters
from .ibuilder import IBuilder from .ibuilder import IBuilder
from .patrolling import PatrollingLayout from .patrolling import PatrollingLayout
from .refuelingflightplan import RefuelingFlightPlan from .refuelingflightplan import RefuelingFlightPlan
@ -98,11 +98,7 @@ class Builder(IBuilder[PackageRefuelingFlightPlan, PatrollingLayout]):
builder = WaypointBuilder(self.flight) builder = WaypointBuilder(self.flight)
tanker_type = self.flight.unit_type altitude = builder.get_patrol_altitude
if tanker_type.patrol_altitude is not None:
altitude = tanker_type.patrol_altitude
else:
altitude = feet(21000)
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude) racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
@ -119,6 +115,7 @@ class Builder(IBuilder[PackageRefuelingFlightPlan, PatrollingLayout]):
arrival=builder.land(self.flight.arrival), arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert), divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
custom_waypoints=list(),
) )
def build(self, dump_debug_info: bool = False) -> PackageRefuelingFlightPlan: def build(self, dump_debug_info: bool = False) -> PackageRefuelingFlightPlan:

View File

@ -31,6 +31,7 @@ class PatrollingLayout(StandardLayout):
if self.divert is not None: if self.divert is not None:
yield self.divert yield self.divert
yield self.bullseye yield self.bullseye
yield from self.custom_waypoints
LayoutT = TypeVar("LayoutT", bound=PatrollingLayout) LayoutT = TypeVar("LayoutT", bound=PatrollingLayout)

View File

@ -27,6 +27,7 @@ class RtbLayout(StandardLayout):
if self.divert is not None: if self.divert is not None:
yield self.divert yield self.divert
yield self.bullseye yield self.bullseye
yield from self.custom_waypoints
class RtbFlightPlan(StandardFlightPlan[RtbLayout]): class RtbFlightPlan(StandardFlightPlan[RtbLayout]):
@ -65,13 +66,13 @@ class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
current_position = self.flight.state.estimate_position() current_position = self.flight.state.estimate_position()
current_altitude, altitude_reference = self.flight.state.estimate_altitude() current_altitude, altitude_reference = self.flight.state.estimate_altitude()
builder = WaypointBuilder(self.flight)
altitude_is_agl = self.flight.is_helo altitude_is_agl = self.flight.is_helo
altitude = ( altitude = (
feet(self.coalition.game.settings.heli_cruise_alt_agl) feet(self.coalition.game.settings.heli_cruise_alt_agl)
if altitude_is_agl if altitude_is_agl
else self.flight.unit_type.preferred_patrol_altitude else builder.get_patrol_altitude
) )
builder = WaypointBuilder(self.flight)
abort_point = builder.nav( abort_point = builder.nav(
current_position, current_altitude, altitude_reference == "RADIO" current_position, current_altitude, altitude_reference == "RADIO"
) )
@ -91,6 +92,7 @@ class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
divert=builder.divert(self.flight.divert), divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
nav_from=[], nav_from=[],
custom_waypoints=list(),
) )
def build(self, dump_debug_info: bool = False) -> RtbFlightPlan: def build(self, dump_debug_info: bool = False) -> RtbFlightPlan:

View File

@ -70,6 +70,9 @@ class StandardLayout(Layout, ABC):
elif waypoint in self.nav_from: elif waypoint in self.nav_from:
self.nav_from.remove(waypoint) self.nav_from.remove(waypoint)
return True return True
elif waypoint in self.custom_waypoints:
self.custom_waypoints.remove(waypoint)
return True
return False return False

View File

@ -11,6 +11,7 @@ from .formationattack import (
from .invalidobjectivelocation import InvalidObjectiveLocation from .invalidobjectivelocation import InvalidObjectiveLocation
from .waypointbuilder import StrikeTarget from .waypointbuilder import StrikeTarget
from ..flightwaypointtype import FlightWaypointType from ..flightwaypointtype import FlightWaypointType
from ...theater.theatergroup import SceneryUnit
class StrikeFlightPlan(FormationAttackFlightPlan): class StrikeFlightPlan(FormationAttackFlightPlan):
@ -28,7 +29,10 @@ class Builder(FormationAttackBuilder[StrikeFlightPlan, FormationAttackLayout]):
targets: list[StrikeTarget] = [] targets: list[StrikeTarget] = []
for idx, unit in enumerate(location.strike_targets): 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) return self._build(FlightWaypointType.INGRESS_STRIKE, targets)

View File

@ -35,6 +35,7 @@ class SweepLayout(LoiterLayout):
if self.divert is not None: if self.divert is not None:
yield self.divert yield self.divert
yield self.bullseye yield self.bullseye
yield from self.custom_waypoints
class SweepFlightPlan(LoiterFlightPlan): class SweepFlightPlan(LoiterFlightPlan):
@ -104,26 +105,27 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
) )
builder = WaypointBuilder(self.flight) builder = WaypointBuilder(self.flight)
start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude) altitude = builder.get_patrol_altitude
start, end = builder.sweep(start_pos, target, altitude)
hold = builder.hold(self._hold_point()) hold = builder.hold(self._hold_point())
return SweepLayout( return SweepLayout(
departure=builder.takeoff(self.flight.departure), departure=builder.takeoff(self.flight.departure),
hold=hold, hold=hold,
nav_to=builder.nav_path( nav_to=builder.nav_path(hold.position, start.position, altitude),
hold.position, start.position, self.doctrine.ingress_altitude
),
nav_from=builder.nav_path( nav_from=builder.nav_path(
end.position, end.position,
self.flight.arrival.position, self.flight.arrival.position,
self.doctrine.ingress_altitude, altitude,
), ),
sweep_start=start, sweep_start=start,
sweep_end=end, sweep_end=end,
arrival=builder.land(self.flight.arrival), arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert), divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
custom_waypoints=list(),
) )
def _hold_point(self) -> Point: def _hold_point(self) -> Point:

View File

@ -1,12 +1,11 @@
from __future__ import annotations from __future__ import annotations
import random
from collections.abc import Iterator from collections.abc import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Type from typing import TYPE_CHECKING, Type
from game.utils import Distance, Speed, feet from game.utils import Distance, Speed
from .capbuilder import CapBuilder from .capbuilder import CapBuilder
from .patrolling import PatrollingFlightPlan, PatrollingLayout from .patrolling import PatrollingFlightPlan, PatrollingLayout
from .waypointbuilder import WaypointBuilder from .waypointbuilder import WaypointBuilder
@ -31,6 +30,7 @@ class TarCapLayout(PatrollingLayout):
if self.divert is not None: if self.divert is not None:
yield self.divert yield self.divert
yield self.bullseye yield self.bullseye
yield from self.custom_waypoints
def delete_waypoint(self, waypoint: FlightWaypoint) -> bool: def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
if waypoint == self.refuel: if waypoint == self.refuel:
@ -95,14 +95,9 @@ class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]):
def layout(self) -> TarCapLayout: def layout(self) -> TarCapLayout:
location = self.package.target 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) builder = WaypointBuilder(self.flight)
patrol_alt = builder.get_patrol_altitude
orbit0p, orbit1p = self.cap_racetrack_for_objective(location, barcap=False) orbit0p, orbit1p = self.cap_racetrack_for_objective(location, barcap=False)
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt) start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
@ -128,6 +123,7 @@ class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]):
arrival=builder.land(self.flight.arrival), arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert), divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
custom_waypoints=list(),
) )
def build(self, dump_debug_info: bool = False) -> TarCapFlightPlan: def build(self, dump_debug_info: bool = False) -> TarCapFlightPlan:

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
from typing import Type 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 .ibuilder import IBuilder
from .patrolling import PatrollingLayout from .patrolling import PatrollingLayout
from .refuelingflightplan import RefuelingFlightPlan from .refuelingflightplan import RefuelingFlightPlan
@ -58,11 +58,7 @@ class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]):
builder = WaypointBuilder(self.flight) builder = WaypointBuilder(self.flight)
tanker_type = self.flight.unit_type altitude = builder.get_patrol_altitude
if tanker_type.patrol_altitude is not None:
altitude = tanker_type.patrol_altitude
else:
altitude = feet(21000)
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude) racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
@ -79,6 +75,7 @@ class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]):
arrival=builder.land(self.flight.arrival), arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert), divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
custom_waypoints=list(),
) )
def build(self, dump_debug_info: bool = False) -> TheaterRefuelingFlightPlan: def build(self, dump_debug_info: bool = False) -> TheaterRefuelingFlightPlan:

View File

@ -26,6 +26,8 @@ from game.theater import (
) )
from game.utils import Distance, meters, nautical_miles, feet from game.utils import Distance, meters, nautical_miles, feet
AGL_TRANSITION_ALT = 5000
if TYPE_CHECKING: if TYPE_CHECKING:
from game.transfers import MultiGroupTransport from game.transfers import MultiGroupTransport
from game.theater.theatergroup import TheaterGroup from game.theater.theatergroup import TheaterGroup
@ -51,11 +53,34 @@ class WaypointBuilder:
self.navmesh = coalition.nav_mesh self.navmesh = coalition.nav_mesh
self.targets = targets self.targets = targets
self._bullseye = coalition.bullseye self._bullseye = coalition.bullseye
self.settings = self.flight.coalition.game.settings
@property @property
def is_helo(self) -> bool: def is_helo(self) -> bool:
return self.flight.is_helo 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: def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
"""Create takeoff waypoint for the given arrival airfield or carrier. """Create takeoff waypoint for the given arrival airfield or carrier.
@ -72,9 +97,7 @@ class WaypointBuilder:
"NAV", "NAV",
FlightWaypointType.NAV, FlightWaypointType.NAV,
position, position,
feet(self.flight.coalition.game.settings.heli_cruise_alt_agl) self.get_cruise_altitude,
if self.is_helo
else self.doctrine.rendezvous_altitude,
description="Enter theater", description="Enter theater",
pretty_name="Enter theater", pretty_name="Enter theater",
) )
@ -101,9 +124,7 @@ class WaypointBuilder:
"NAV", "NAV",
FlightWaypointType.NAV, FlightWaypointType.NAV,
position, position,
feet(self.flight.coalition.game.settings.heli_cruise_alt_agl) self.get_cruise_altitude,
if self.is_helo
else self.doctrine.rendezvous_altitude,
description="Exit theater", description="Exit theater",
pretty_name="Exit theater", pretty_name="Exit theater",
) )
@ -129,14 +150,10 @@ class WaypointBuilder:
return None return None
position = divert.position position = divert.position
altitude_type: AltitudeReference altitude_type: AltitudeReference = "BARO"
if isinstance(divert, OffMapSpawn): if isinstance(divert, OffMapSpawn):
altitude = ( altitude = self.get_cruise_altitude
feet(self.flight.coalition.game.settings.heli_cruise_alt_agl) altitude_type = "RADIO" if self.is_helo else altitude_type
if self.is_helo
else self.doctrine.rendezvous_altitude
)
altitude_type = "BARO"
else: else:
altitude = meters(0) altitude = meters(0)
altitude_type = "RADIO" altitude_type = "RADIO"
@ -166,16 +183,15 @@ class WaypointBuilder:
def hold(self, position: Point) -> FlightWaypoint: def hold(self, position: Point) -> FlightWaypoint:
alt_type: AltitudeReference = "BARO" alt_type: AltitudeReference = "BARO"
if self.is_helo: if self.is_helo or self.get_combat_altitude.feet <= AGL_TRANSITION_ALT:
alt_type = "RADIO" alt_type = "RADIO"
return FlightWaypoint( return FlightWaypoint(
"HOLD", "HOLD",
FlightWaypointType.LOITER, FlightWaypointType.LOITER,
position, position,
feet(self.flight.coalition.game.settings.heli_cruise_alt_agl) # TODO: dedicated altitude setting for holding
if self.is_helo self.get_cruise_altitude if self.is_helo else self.get_combat_altitude,
else self.doctrine.ingress_altitude,
alt_type, alt_type,
description="Wait until push time", description="Wait until push time",
pretty_name="Hold", pretty_name="Hold",
@ -183,16 +199,14 @@ class WaypointBuilder:
def join(self, position: Point) -> FlightWaypoint: def join(self, position: Point) -> FlightWaypoint:
alt_type: AltitudeReference = "BARO" alt_type: AltitudeReference = "BARO"
if self.is_helo: if self.is_helo or self.get_cruise_altitude.feet <= AGL_TRANSITION_ALT:
alt_type = "RADIO" alt_type = "RADIO"
return FlightWaypoint( return FlightWaypoint(
"JOIN", "JOIN",
FlightWaypointType.JOIN, FlightWaypointType.JOIN,
position, position,
feet(self.flight.coalition.game.settings.heli_cruise_alt_agl) self.get_cruise_altitude,
if self.is_helo
else self.doctrine.ingress_altitude,
alt_type, alt_type,
description="Rendezvous with package", description="Rendezvous with package",
pretty_name="Join", pretty_name="Join",
@ -200,16 +214,14 @@ class WaypointBuilder:
def refuel(self, position: Point) -> FlightWaypoint: def refuel(self, position: Point) -> FlightWaypoint:
alt_type: AltitudeReference = "BARO" alt_type: AltitudeReference = "BARO"
if self.is_helo: if self.is_helo or self.get_cruise_altitude.feet <= AGL_TRANSITION_ALT:
alt_type = "RADIO" alt_type = "RADIO"
return FlightWaypoint( return FlightWaypoint(
"REFUEL", "REFUEL",
FlightWaypointType.REFUEL, FlightWaypointType.REFUEL,
position, position,
feet(self.flight.coalition.game.settings.heli_cruise_alt_agl) self.get_cruise_altitude,
if self.is_helo
else self.doctrine.ingress_altitude,
alt_type, alt_type,
description="Refuel from tanker", description="Refuel from tanker",
pretty_name="Refuel", pretty_name="Refuel",
@ -217,16 +229,14 @@ class WaypointBuilder:
def split(self, position: Point) -> FlightWaypoint: def split(self, position: Point) -> FlightWaypoint:
alt_type: AltitudeReference = "BARO" alt_type: AltitudeReference = "BARO"
if self.is_helo: if self.is_helo or self.get_combat_altitude.feet <= AGL_TRANSITION_ALT:
alt_type = "RADIO" alt_type = "RADIO"
return FlightWaypoint( return FlightWaypoint(
"SPLIT", "SPLIT",
FlightWaypointType.SPLIT, FlightWaypointType.SPLIT,
position, position,
feet(self.flight.coalition.game.settings.heli_combat_alt_agl) self.get_combat_altitude,
if self.is_helo
else self.doctrine.ingress_altitude,
alt_type, alt_type,
description="Depart from package", description="Depart from package",
pretty_name="Split", pretty_name="Split",
@ -238,7 +248,7 @@ class WaypointBuilder:
position: Point, position: Point,
objective: MissionTarget, objective: MissionTarget,
) -> FlightWaypoint: ) -> FlightWaypoint:
alt = self.doctrine.ingress_altitude alt = self.get_combat_altitude
alt_type: AltitudeReference = "BARO" alt_type: AltitudeReference = "BARO"
if self.is_helo or self.flight.is_hercules: if self.is_helo or self.flight.is_hercules:
alt_type = "RADIO" alt_type = "RADIO"
@ -247,6 +257,8 @@ class WaypointBuilder:
if self.is_helo if self.is_helo
else feet(1000) else feet(1000)
) )
elif alt.feet <= AGL_TRANSITION_ALT:
alt_type = "RADIO"
heading = objective.position.heading_between_point(position) heading = objective.position.heading_between_point(position)
@ -265,16 +277,14 @@ class WaypointBuilder:
def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint: def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint:
alt_type: AltitudeReference = "BARO" alt_type: AltitudeReference = "BARO"
if self.is_helo: if self.is_helo or self.get_combat_altitude.feet <= AGL_TRANSITION_ALT:
alt_type = "RADIO" alt_type = "RADIO"
return FlightWaypoint( return FlightWaypoint(
"EGRESS", "EGRESS",
FlightWaypointType.EGRESS, FlightWaypointType.EGRESS,
position, position,
feet(self.flight.coalition.game.settings.heli_combat_alt_agl) self.get_combat_altitude,
if self.is_helo
else self.doctrine.ingress_altitude,
alt_type, alt_type,
description=f"EGRESS from {target.name}", description=f"EGRESS from {target.name}",
pretty_name=f"EGRESS from {target.name}", pretty_name=f"EGRESS from {target.name}",
@ -312,11 +322,15 @@ class WaypointBuilder:
return self._target_area(f"STRIKE {target.name}", target) return self._target_area(f"STRIKE {target.name}", target)
def sead_area(self, target: MissionTarget) -> FlightWaypoint: 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( return self._target_area(
f"SEAD on {target.name}", f"SEAD on {target.name}",
target, target,
altitude=self.doctrine.ingress_altitude, altitude=self.get_combat_altitude,
alt_type="BARO", alt_type=alt_type,
) )
def dead_area(self, target: MissionTarget) -> FlightWaypoint: def dead_area(self, target: MissionTarget) -> FlightWaypoint:
@ -387,11 +401,13 @@ class WaypointBuilder:
position: Position of the waypoint. position: Position of the waypoint.
altitude: Altitude of the racetrack. altitude: Altitude of the racetrack.
""" """
baro: AltitudeReference = "BARO"
return FlightWaypoint( return FlightWaypoint(
"RACETRACK START", "RACETRACK START",
FlightWaypointType.PATROL_TRACK, FlightWaypointType.PATROL_TRACK,
position, position,
altitude, altitude,
"RADIO" if altitude.feet <= AGL_TRANSITION_ALT else baro,
description="Orbit between this point and the next point", description="Orbit between this point and the next point",
pretty_name="Race-track start", pretty_name="Race-track start",
) )
@ -404,11 +420,13 @@ class WaypointBuilder:
position: Position of the waypoint. position: Position of the waypoint.
altitude: Altitude of the racetrack. altitude: Altitude of the racetrack.
""" """
baro: AltitudeReference = "BARO"
return FlightWaypoint( return FlightWaypoint(
"RACETRACK END", "RACETRACK END",
FlightWaypointType.PATROL, FlightWaypointType.PATROL,
position, position,
altitude, altitude,
"RADIO" if altitude.feet <= AGL_TRANSITION_ALT else baro,
description="Orbit between this point and the previous point", description="Orbit between this point and the previous point",
pretty_name="Race-track end", pretty_name="Race-track end",
) )
@ -436,42 +454,39 @@ class WaypointBuilder:
start: Position of the waypoint. start: Position of the waypoint.
altitude: Altitude of the racetrack. altitude: Altitude of the racetrack.
""" """
baro: AltitudeReference = "BARO"
return FlightWaypoint( return FlightWaypoint(
"ORBIT", "ORBIT",
FlightWaypointType.LOITER, FlightWaypointType.LOITER,
start, start,
altitude, altitude,
"RADIO" if altitude.feet <= AGL_TRANSITION_ALT else baro,
description="Anchor and hold at this point", description="Anchor and hold at this point",
pretty_name="Orbit", pretty_name="Orbit",
) )
def sead_search(self, target: MissionTarget) -> FlightWaypoint: def sead_search(self, target: MissionTarget) -> FlightWaypoint:
hold = self._sead_search_point(target) hold = self._sead_search_point(target)
baro: AltitudeReference = "BARO"
return FlightWaypoint( return FlightWaypoint(
"SEAD Search", "SEAD Search",
FlightWaypointType.NAV, FlightWaypointType.NAV,
hold, hold,
feet(self.flight.coalition.game.settings.heli_combat_alt_agl) self.get_combat_altitude,
if self.is_helo "RADIO" if self.get_combat_altitude.feet <= AGL_TRANSITION_ALT else baro,
else self.doctrine.ingress_altitude,
alt_type="BARO",
description="Anchor and search from this point", description="Anchor and search from this point",
pretty_name="SEAD Search", pretty_name="SEAD Search",
) )
def sead_sweep(self, target: MissionTarget) -> FlightWaypoint: def sead_sweep(self, target: MissionTarget) -> FlightWaypoint:
hold = self._sead_search_point(target) hold = self._sead_search_point(target)
baro: AltitudeReference = "BARO"
return FlightWaypoint( return FlightWaypoint(
"SEAD Sweep", "SEAD Sweep",
FlightWaypointType.NAV, FlightWaypointType.NAV,
hold, hold,
feet(self.flight.coalition.game.settings.heli_combat_alt_agl) self.get_combat_altitude,
if self.is_helo "RADIO" if self.get_combat_altitude.feet <= AGL_TRANSITION_ALT else baro,
else self.doctrine.ingress_altitude,
alt_type="BARO",
description="Anchor and search from this point", description="Anchor and search from this point",
pretty_name="SEAD Sweep", pretty_name="SEAD Sweep",
) )
@ -499,15 +514,16 @@ class WaypointBuilder:
) )
return hold return hold
def escort_hold(self, start: Point, altitude: Distance) -> FlightWaypoint: def escort_hold(self, start: Point) -> FlightWaypoint:
"""Creates custom waypoint for escort flights that need to hold. """Creates custom waypoint for escort flights that need to hold.
Args: Args:
start: Position of the waypoint. start: Position of the waypoint.
altitude: Altitude of the holding pattern.
""" """
altitude = self.get_combat_altitude
alt_type: Literal["BARO", "RADIO"] = "BARO" alt_type: Literal["BARO", "RADIO"] = "BARO"
if self.is_helo: if self.is_helo or altitude.feet <= AGL_TRANSITION_ALT:
alt_type = "RADIO" alt_type = "RADIO"
return FlightWaypoint( return FlightWaypoint(
@ -528,11 +544,13 @@ class WaypointBuilder:
position: Position of the waypoint. position: Position of the waypoint.
altitude: Altitude of the sweep in meters. altitude: Altitude of the sweep in meters.
""" """
baro: AltitudeReference = "BARO"
return FlightWaypoint( return FlightWaypoint(
"SWEEP START", "SWEEP START",
FlightWaypointType.INGRESS_SWEEP, FlightWaypointType.INGRESS_SWEEP,
position, position,
altitude, altitude,
"RADIO" if altitude.feet <= AGL_TRANSITION_ALT else baro,
description="Proceed to the target and engage enemy aircraft", description="Proceed to the target and engage enemy aircraft",
pretty_name="Sweep start", pretty_name="Sweep start",
) )
@ -545,11 +563,13 @@ class WaypointBuilder:
position: Position of the waypoint. position: Position of the waypoint.
altitude: Altitude of the sweep in meters. altitude: Altitude of the sweep in meters.
""" """
baro: AltitudeReference = "BARO"
return FlightWaypoint( return FlightWaypoint(
"SWEEP END", "SWEEP END",
FlightWaypointType.EGRESS, FlightWaypointType.EGRESS,
position, position,
altitude, altitude,
"RADIO" if altitude.feet <= AGL_TRANSITION_ALT else baro,
description="End of sweep", description="End of sweep",
pretty_name="Sweep end", pretty_name="Sweep end",
) )
@ -578,7 +598,7 @@ class WaypointBuilder:
target: The mission target. target: The mission target.
""" """
alt_type: AltitudeReference = "BARO" alt_type: AltitudeReference = "BARO"
if self.is_helo: if self.is_helo or self.get_combat_altitude.feet <= AGL_TRANSITION_ALT:
alt_type = "RADIO" alt_type = "RADIO"
# This would preferably be no points at all, and instead the Escort task # This would preferably be no points at all, and instead the Escort task
@ -592,9 +612,7 @@ class WaypointBuilder:
"TARGET", "TARGET",
FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_GROUP_LOC,
target.position, target.position,
feet(self.flight.coalition.game.settings.heli_combat_alt_agl) self.get_combat_altitude,
if self.is_helo
else self.doctrine.ingress_altitude,
alt_type, alt_type,
description="Escort the package", description="Escort the package",
pretty_name="Target area", pretty_name="Target area",
@ -616,17 +634,19 @@ class WaypointBuilder:
pretty_name="Pick-up zone", pretty_name="Pick-up zone",
) )
@staticmethod def dropoff_zone(self, drop_off: MissionTarget) -> FlightWaypoint:
def dropoff_zone(drop_off: MissionTarget) -> FlightWaypoint:
"""Creates a dropoff landing zone waypoint """Creates a dropoff landing zone waypoint
This waypoint is used to generate the Trigger Zone used for AirAssault and This waypoint is used to generate the Trigger Zone used for AirAssault and
AirLift using the CTLD plugin (see LogisticsGenerator) 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( return FlightWaypoint(
"DROPOFFZONE", "DROPOFFZONE",
FlightWaypointType.DROPOFF_ZONE, FlightWaypointType.DROPOFF_ZONE,
drop_off.position, drop_off.position,
meters(0), altitude,
"RADIO", "RADIO",
description=f"Drop off cargo at {drop_off.name}", description=f"Drop off cargo at {drop_off.name}",
pretty_name="Drop-off zone", pretty_name="Drop-off zone",
@ -660,7 +680,7 @@ class WaypointBuilder:
altitude_is_agl: True for altitude is AGL. False if altitude is MSL. altitude_is_agl: True for altitude is AGL. False if altitude is MSL.
""" """
alt_type: AltitudeReference = "BARO" alt_type: AltitudeReference = "BARO"
if altitude_is_agl: if altitude_is_agl or altitude.feet <= AGL_TRANSITION_ALT:
alt_type = "RADIO" alt_type = "RADIO"
return FlightWaypoint( return FlightWaypoint(

View File

@ -137,11 +137,15 @@ class Loadout:
continue continue
name = payload["name"] name = payload["name"]
pylons = payload["pylons"] pylons = payload["pylons"]
try:
yield Loadout( yield Loadout(
name, name,
{p["num"]: Weapon.with_clsid(p["CLSID"]) for p in pylons.values()}, {p["num"]: Weapon.with_clsid(p["CLSID"]) for p in pylons.values()},
date=None, date=None,
) )
except KeyError:
# invalid loadout
continue
@staticmethod @staticmethod
def valid_payload(pylons: Dict[int, Dict[str, str]]) -> bool: def valid_payload(pylons: Dict[int, Dict[str, str]]) -> bool:

View File

@ -5,7 +5,7 @@ import logging
from collections.abc import Iterator from collections.abc import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Tuple from typing import Any, Dict, Tuple, Optional
import yaml import yaml
from packaging.version import Version from packaging.version import Version
@ -19,8 +19,10 @@ from game.theater.iadsnetwork.iadsnetwork import IadsNetwork
from game.theater.theaterloader import TheaterLoader from game.theater.theaterloader import TheaterLoader
from game.version import CAMPAIGN_FORMAT_VERSION from game.version import CAMPAIGN_FORMAT_VERSION
from .campaignairwingconfig import CampaignAirWingConfig from .campaignairwingconfig import CampaignAirWingConfig
from .campaigncarrierconfig import CampaignCarrierConfig
from .campaigngroundconfig import TgoConfig from .campaigngroundconfig import TgoConfig
from .mizcampaignloader import MizCampaignLoader from .mizcampaignloader import MizCampaignLoader
from ..factions import FACTIONS, Faction
PERF_FRIENDLY = 0 PERF_FRIENDLY = 0
PERF_MEDIUM = 1 PERF_MEDIUM = 1
@ -90,6 +92,16 @@ class Campaign:
f"Invalid value for recommended_start_date in {path}: {start_date_raw}" f"Invalid value for recommended_start_date in {path}: {start_date_raw}"
) )
player_faction = data.get("recommended_player_faction", "USA 2005")
if isinstance(player_faction, dict):
faction_name = cls.register_faction(campaign_file.name, player_faction)
player_faction = faction_name if faction_name else "USA 2005"
enemy_faction = data.get("recommended_enemy_faction", "Russia 1990")
if isinstance(enemy_faction, dict):
faction_name = cls.register_faction(campaign_file.name, enemy_faction)
enemy_faction = faction_name if faction_name else "Russia 1990"
return cls( return cls(
data["name"], data["name"],
TheaterLoader(data["theater"].lower()).menu_thumbnail_dcs_relative_path, TheaterLoader(data["theater"].lower()).menu_thumbnail_dcs_relative_path,
@ -97,8 +109,8 @@ class Campaign:
data.get("authors", "???"), data.get("authors", "???"),
data.get("description", ""), data.get("description", ""),
(version.major, version.minor), (version.major, version.minor),
data.get("recommended_player_faction", "USA 2005"), player_faction,
data.get("recommended_enemy_faction", "Russia 1990"), enemy_faction,
start_date, start_date,
start_time, start_time,
data.get("recommended_player_money", DEFAULT_BUDGET), data.get("recommended_player_money", DEFAULT_BUDGET),
@ -112,6 +124,19 @@ class Campaign:
data.get("settings", {}), data.get("settings", {}),
) )
@classmethod
def register_faction(
cls, filename: str, player_faction: dict[str, Any]
) -> Optional[str]:
try:
f = Faction.from_dict(player_faction)
FACTIONS.factions[f.name] = f
logging.info(f"Loaded faction from campaign: {filename}")
return f.name
except Exception:
logging.exception(f"Unable to load faction from campaign: {filename}")
return None
def load_theater(self, advanced_iads: bool) -> ConflictTheater: def load_theater(self, advanced_iads: bool) -> ConflictTheater:
t = TheaterLoader(self.data["theater"].lower()).load() t = TheaterLoader(self.data["theater"].lower()).load()
@ -140,6 +165,13 @@ class Campaign:
return CampaignAirWingConfig({}) return CampaignAirWingConfig({})
return CampaignAirWingConfig.from_campaign_data(squadron_data, theater) return CampaignAirWingConfig.from_campaign_data(squadron_data, theater)
def load_carrier_config(self) -> CampaignCarrierConfig:
try:
carrier_data = self.data["carriers"]
except KeyError:
return CampaignCarrierConfig({})
return CampaignCarrierConfig.from_campaign_data(carrier_data)
def load_ground_forces_config(self) -> TgoConfig: def load_ground_forces_config(self) -> TgoConfig:
ground_forces = self.data.get("ground_forces", {}) ground_forces = self.data.get("ground_forces", {})
if not ground_forces: if not ground_forces:

View File

@ -0,0 +1,42 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from typing import Any, TYPE_CHECKING
from game.dcs.shipunittype import ShipUnitType
if TYPE_CHECKING:
pass
@dataclass(frozen=True)
class CarrierConfig:
preferred_name: str
preferred_type: ShipUnitType
@classmethod
def from_data(cls, data: dict[str, Any]) -> CarrierConfig:
return CarrierConfig(
str(data["preferred_name"]), ShipUnitType.named(data["preferred_type"])
)
@dataclass(frozen=True)
class CampaignCarrierConfig:
by_original_name: dict[str, CarrierConfig]
@classmethod
def from_campaign_data(cls, data: dict[str, Any]) -> CampaignCarrierConfig:
by_original_name: dict[str, CarrierConfig] = defaultdict()
for original_name, carrier_config_data in data.items():
try:
carrier_config = CarrierConfig.from_data(carrier_config_data)
by_original_name[original_name] = carrier_config
except KeyError:
logging.warning(
f"Skipping invalid carrier config for '{original_name}'"
)
return CampaignCarrierConfig(by_original_name)

View File

@ -47,6 +47,7 @@ class SquadronDefGenerator:
role="Flying Squadron", role="Flying Squadron",
aircraft=aircraft, aircraft=aircraft,
livery=None, livery=None,
livery_set=[],
auto_assignable_mission_types=set(aircraft.iter_task_capabilities()), auto_assignable_mission_types=set(aircraft.iter_task_capabilities()),
radio_presets={}, radio_presets={},
operating_bases=OperatingBases.default_for_aircraft(aircraft), operating_bases=OperatingBases.default_for_aircraft(aircraft),

View File

@ -38,7 +38,7 @@ class PackageBuilder:
self.laser_code_registry = laser_code_registry self.laser_code_registry = laser_code_registry
self.start_type = start_type 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. """Allocates aircraft for the given flight and adds them to the package.
If no suitable aircraft are available, False is returned. If the failed If no suitable aircraft are available, False is returned. If the failed
@ -55,6 +55,7 @@ class PackageBuilder:
heli, heli,
this_turn=True, this_turn=True,
preferred_type=plan.preferred_type, preferred_type=plan.preferred_type,
ignore_range=ignore_range,
) )
if squadron is None: if squadron is None:
return False return False

View File

@ -81,8 +81,9 @@ class PackageFulfiller:
builder: PackageBuilder, builder: PackageBuilder,
missing_types: Set[FlightType], missing_types: Set[FlightType],
purchase_multiplier: int, purchase_multiplier: int,
ignore_range: bool = False,
) -> None: ) -> None:
if not builder.plan_flight(flight): if not builder.plan_flight(flight, ignore_range):
pf = builder.package.primary_flight pf = builder.package.primary_flight
heli = pf.is_helo if pf else False heli = pf.is_helo if pf else False
missing_types.add(flight.task) missing_types.add(flight.task)
@ -138,6 +139,7 @@ class PackageFulfiller:
purchase_multiplier: int, purchase_multiplier: int,
now: datetime, now: datetime,
tracer: MultiEventTracer, tracer: MultiEventTracer,
ignore_range: bool = False,
) -> Optional[Package]: ) -> Optional[Package]:
"""Allocates aircraft for a proposed mission and adds it to the ATO.""" """Allocates aircraft for a proposed mission and adds it to the ATO."""
builder = PackageBuilder( builder = PackageBuilder(
@ -175,6 +177,7 @@ class PackageFulfiller:
builder, builder,
missing_types, missing_types,
purchase_multiplier, purchase_multiplier,
ignore_range,
) )
if missing_types: if missing_types:

View File

@ -26,8 +26,6 @@ class Doctrine:
strike: bool strike: bool
antiship: bool antiship: bool
rendezvous_altitude: Distance
#: The minimum distance between the departure airfield and the hold point. #: The minimum distance between the departure airfield and the hold point.
hold_distance: Distance hold_distance: Distance
@ -46,11 +44,14 @@ class Doctrine:
#: target. #: target.
min_ingress_distance: Distance min_ingress_distance: Distance
ingress_altitude: Distance
min_patrol_altitude: Distance min_patrol_altitude: Distance
max_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. #: The duration that CAP flights will remain on-station.
cap_duration: timedelta cap_duration: timedelta
@ -97,16 +98,17 @@ MODERN_DOCTRINE = Doctrine(
sead=True, sead=True,
strike=True, strike=True,
antiship=True, antiship=True,
rendezvous_altitude=feet(25000),
hold_distance=nautical_miles(25), hold_distance=nautical_miles(25),
push_distance=nautical_miles(20), push_distance=nautical_miles(20),
join_distance=nautical_miles(20), join_distance=nautical_miles(20),
max_ingress_distance=nautical_miles(45), max_ingress_distance=nautical_miles(45),
min_ingress_distance=nautical_miles(10), min_ingress_distance=nautical_miles(10),
ingress_altitude=feet(20000),
min_patrol_altitude=feet(15000), min_patrol_altitude=feet(15000),
max_patrol_altitude=feet(33000), 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_duration=timedelta(minutes=30),
cap_min_track_length=nautical_miles(15), cap_min_track_length=nautical_miles(15),
cap_max_track_length=nautical_miles(40), cap_max_track_length=nautical_miles(40),
@ -140,16 +142,17 @@ COLDWAR_DOCTRINE = Doctrine(
sead=True, sead=True,
strike=True, strike=True,
antiship=True, antiship=True,
rendezvous_altitude=feet(22000),
hold_distance=nautical_miles(15), hold_distance=nautical_miles(15),
push_distance=nautical_miles(10), push_distance=nautical_miles(10),
join_distance=nautical_miles(10), join_distance=nautical_miles(10),
max_ingress_distance=nautical_miles(30), max_ingress_distance=nautical_miles(30),
min_ingress_distance=nautical_miles(10), min_ingress_distance=nautical_miles(10),
ingress_altitude=feet(18000),
min_patrol_altitude=feet(10000), min_patrol_altitude=feet(10000),
max_patrol_altitude=feet(24000), 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_duration=timedelta(minutes=30),
cap_min_track_length=nautical_miles(12), cap_min_track_length=nautical_miles(12),
cap_max_track_length=nautical_miles(24), cap_max_track_length=nautical_miles(24),
@ -186,13 +189,14 @@ WWII_DOCTRINE = Doctrine(
hold_distance=nautical_miles(10), hold_distance=nautical_miles(10),
push_distance=nautical_miles(5), push_distance=nautical_miles(5),
join_distance=nautical_miles(5), join_distance=nautical_miles(5),
rendezvous_altitude=feet(10000),
max_ingress_distance=nautical_miles(7), max_ingress_distance=nautical_miles(7),
min_ingress_distance=nautical_miles(5), min_ingress_distance=nautical_miles(5),
ingress_altitude=feet(8000),
min_patrol_altitude=feet(4000), min_patrol_altitude=feet(4000),
max_patrol_altitude=feet(15000), 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_duration=timedelta(minutes=30),
cap_min_track_length=nautical_miles(8), cap_min_track_length=nautical_miles(8),
cap_max_track_length=nautical_miles(18), cap_max_track_length=nautical_miles(18),

View File

@ -132,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) @dataclass(frozen=True)
class FuelConsumption: class FuelConsumption:
#: The estimated taxi fuel requirement, in pounds. #: The estimated taxi fuel requirement, in pounds.
@ -182,6 +197,9 @@ class AircraftType(UnitType[Type[FlyingType]]):
patrol_altitude: Optional[Distance] patrol_altitude: Optional[Distance]
patrol_speed: Optional[Speed] 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- #: The maximum range between the origin airfield and the target for which the auto-
#: planner will consider this aircraft usable for a mission. #: planner will consider this aircraft usable for a mission.
max_mission_range: Distance max_mission_range: Distance
@ -245,33 +263,13 @@ class AircraftType(UnitType[Type[FlyingType]]):
def max_speed(self) -> Speed: def max_speed(self) -> Speed:
return kph(self.dcs_unit_type.max_speed) return kph(self.dcs_unit_type.max_speed)
@property @cached_property
def preferred_patrol_altitude(self) -> Distance: def preferred_patrol_altitude(self) -> Distance:
if self.patrol_altitude is not None: if self.patrol_altitude:
return self.patrol_altitude return self.patrol_altitude
else: else:
# Estimate based on max speed. # TODO: somehow make the upper and lower limit configurable
# Aircaft with max speed 600 kph will prefer patrol at 10 000 ft return self.preferred_altitude(10, 33, "patrol")
# 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),
)
def preferred_patrol_speed(self, altitude: Distance) -> Speed: def preferred_patrol_speed(self, altitude: Distance) -> Speed:
"""Preferred true airspeed when patrolling""" """Preferred true airspeed when patrolling"""
@ -309,6 +307,46 @@ class AircraftType(UnitType[Type[FlyingType]]):
) )
return min(Speed.from_mach(0.35, altitude), 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: def alloc_flight_radio(self, radio_registry: RadioRegistry) -> RadioFrequency:
from game.radio.radios import ChannelInUseError, kHz from game.radio.radios import ChannelInUseError, kHz
@ -442,6 +480,7 @@ class AircraftType(UnitType[Type[FlyingType]]):
radio_config = RadioConfig.from_data(data.get("radios", {})) radio_config = RadioConfig.from_data(data.get("radios", {}))
patrol_config = PatrolConfig.from_data(data.get("patrol", {})) patrol_config = PatrolConfig.from_data(data.get("patrol", {}))
altitudes_config = AltitudesConfig.from_data(data.get("altitudes", {}))
try: try:
mission_range = nautical_miles(int(data["max_range"])) mission_range = nautical_miles(int(data["max_range"]))
@ -510,6 +549,8 @@ class AircraftType(UnitType[Type[FlyingType]]):
max_group_size=data.get("max_group_size", aircraft.group_size_max), max_group_size=data.get("max_group_size", aircraft.group_size_max),
patrol_altitude=patrol_config.altitude, patrol_altitude=patrol_config.altitude,
patrol_speed=patrol_config.speed, patrol_speed=patrol_config.speed,
cruise_altitude=altitudes_config.cruise,
combat_altitude=altitudes_config.combat,
max_mission_range=mission_range, max_mission_range=mission_range,
fuel_consumption=fuel_consumption, fuel_consumption=fuel_consumption,
default_livery=data.get("default_livery"), default_livery=data.get("default_livery"),

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import itertools import itertools
import logging import logging
from collections import defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import cached_property from functools import cached_property
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING, Set from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING, Set
@ -93,11 +94,8 @@ class Faction:
# Required mods or asset packs # Required mods or asset packs
requirements: Dict[str, str] = field(default_factory=dict) requirements: Dict[str, str] = field(default_factory=dict)
# Possible carrier names # Possible carrier units mapped to names
carrier_names: Set[str] = field(default_factory=set) carriers: Dict[ShipUnitType, Set[str]] = field(default_factory=dict)
# Possible helicopter carrier names
helicopter_carrier_names: Set[str] = field(default_factory=set)
# Available Naval Units # Available Naval Units
naval_units: Set[ShipUnitType] = field(default_factory=set) naval_units: Set[ShipUnitType] = field(default_factory=set)
@ -241,8 +239,30 @@ class Faction:
faction.requirements = json.get("requirements", {}) faction.requirements = json.get("requirements", {})
faction.carrier_names = json.get("carrier_names", []) # First try to load the carriers in the new format which
faction.helicopter_carrier_names = json.get("helicopter_carrier_names", []) # specifies different names for different carrier types
loaded_carriers = load_carriers(json)
carriers: List[ShipUnitType] = [
unit
for unit in faction.naval_units
if unit.unit_class
in [
UnitClass.AIRCRAFT_CARRIER,
UnitClass.HELICOPTER_CARRIER,
]
]
carrier_names = json.get("carrier_names", [])
helicopter_carrier_names = json.get("helicopter_carrier_names", [])
for c in carriers:
if c.variant_id not in loaded_carriers:
if c.unit_class == UnitClass.AIRCRAFT_CARRIER:
loaded_carriers[c] = carrier_names
elif c.unit_class == UnitClass.HELICOPTER_CARRIER:
loaded_carriers[c] = helicopter_carrier_names
faction.carriers = loaded_carriers
faction.naval_units.union(faction.carriers.keys())
faction.has_jtac = json.get("has_jtac", False) faction.has_jtac = json.get("has_jtac", False)
jtac_name = json.get("jtac_unit", None) jtac_name = json.get("jtac_unit", None)
@ -340,6 +360,8 @@ class Faction:
if not mod_settings.f4bc_phantom: if not mod_settings.f4bc_phantom:
self.remove_aircraft("VSN_F4B") self.remove_aircraft("VSN_F4B")
self.remove_aircraft("VSN_F4C") self.remove_aircraft("VSN_F4C")
if not mod_settings.f9f_panther:
self.remove_aircraft("VSN_F9F")
if not mod_settings.f15d_baz: if not mod_settings.f15d_baz:
self.remove_aircraft("F-15D") self.remove_aircraft("F-15D")
if not mod_settings.f_15_idf: if not mod_settings.f_15_idf:
@ -381,6 +403,8 @@ class Faction:
self.remove_aircraft("JAS39Gripen") self.remove_aircraft("JAS39Gripen")
self.remove_aircraft("JAS39Gripen_BVR") self.remove_aircraft("JAS39Gripen_BVR")
self.remove_aircraft("JAS39Gripen_AG") self.remove_aircraft("JAS39Gripen_AG")
if not mod_settings.super_etendard:
self.remove_aircraft("VSN_SEM")
if not mod_settings.su30_flanker_h: if not mod_settings.su30_flanker_h:
self.remove_aircraft("Su-30MKA") self.remove_aircraft("Su-30MKA")
self.remove_aircraft("Su-30MKI") self.remove_aircraft("Su-30MKI")
@ -592,3 +616,14 @@ def load_all_ships(data: list[str]) -> List[Type[ShipType]]:
if item is not None: if item is not None:
items.append(item) items.append(item)
return items return items
def load_carriers(json: Dict[str, Any]) -> Dict[ShipUnitType, Set[str]]:
# Load carriers
items: Dict[ShipUnitType, Set[str]] = defaultdict(Set[str])
carriers = json.get("carriers", {})
for carrier_shiptype, shipnames in carriers.items():
shiptype = ShipUnitType.named(carrier_shiptype)
if shiptype is not None:
items[shiptype] = shipnames
return items

View File

@ -5,6 +5,8 @@ import logging
from pathlib import Path from pathlib import Path
from typing import Dict, Iterator, List, Optional, Type from typing import Dict, Iterator, List, Optional, Type
import yaml
from game import persistency from game import persistency
from game.factions.faction import Faction from game.factions.faction import Faction
@ -27,7 +29,11 @@ class FactionLoader:
@staticmethod @staticmethod
def find_faction_files_in(path: Path) -> List[Path]: def find_faction_files_in(path: Path) -> List[Path]:
return [f for f in path.glob("*.json") if f.is_file()] return (
[f for f in path.glob("*.json") if f.is_file()]
+ [f for f in path.glob("*.yaml") if f.is_file()]
+ [f for f in path.glob("*.yml") if f.is_file()]
)
@classmethod @classmethod
def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]: def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]:
@ -40,6 +46,9 @@ class FactionLoader:
for f in files: for f in files:
try: try:
with f.open("r", encoding="utf-8") as fdata: with f.open("r", encoding="utf-8") as fdata:
if "yml" in f.name or "yaml" in f.name:
data = yaml.safe_load(fdata)
else:
data = json.load(fdata) data = json.load(fdata)
factions[data["name"]] = Faction.from_dict(data) factions[data["name"]] = Faction.from_dict(data)
logging.info("Loaded faction : " + str(f)) logging.info("Loaded faction : " + str(f))

View File

@ -52,7 +52,7 @@ class Migrator:
continue continue
found = False found = False
for d in doctrines: for d in doctrines:
if c.faction.doctrine.rendezvous_altitude == d.rendezvous_altitude: if c.faction.doctrine.max_patrol_altitude == d.max_patrol_altitude:
c.faction.doctrine = d c.faction.doctrine = d
found = True found = True
break break
@ -78,6 +78,7 @@ class Migrator:
def _update_control_points(self) -> None: def _update_control_points(self) -> None:
is_sinai = self.game.theater.terrain.name == "SinaiMap" is_sinai = self.game.theater.terrain.name == "SinaiMap"
for cp in self.game.theater.controlpoints: for cp in self.game.theater.controlpoints:
cp.release_parking_slots()
is_carrier = cp.is_carrier is_carrier = cp.is_carrier
is_lha = cp.is_lha is_lha = cp.is_lha
is_fob = cp.category == "fob" is_fob = cp.category == "fob"
@ -107,6 +108,7 @@ class Migrator:
layout = f.flight_plan.layout layout = f.flight_plan.layout
try_set_attr(layout, "nav_to", []) try_set_attr(layout, "nav_to", [])
try_set_attr(layout, "nav_from", []) try_set_attr(layout, "nav_from", [])
try_set_attr(layout, "custom_waypoints", [])
if f.flight_type == FlightType.CAS: if f.flight_type == FlightType.CAS:
try_set_attr(layout, "ingress", None) try_set_attr(layout, "ingress", None)
if isinstance(layout, FormationLayout): if isinstance(layout, FormationLayout):
@ -121,10 +123,14 @@ class Migrator:
try_set_attr(f, "tacan") try_set_attr(f, "tacan")
try_set_attr(f, "tcn_name") try_set_attr(f, "tcn_name")
try_set_attr(f, "fuel", f.unit_type.max_fuel) try_set_attr(f, "fuel", f.unit_type.max_fuel)
try_set_attr(f, "plane_altitude_offset", 0)
try_set_attr(f, "use_same_livery_for_all_members", True)
if f.package in f.squadron.coalition.ato.packages: if f.package in f.squadron.coalition.ato.packages:
self._update_flight_plan(f) self._update_flight_plan(f)
else: else:
to_remove.append(f.id) to_remove.append(f.id)
for m in f.roster.members:
try_set_attr(m, "livery", None)
for fid in to_remove: for fid in to_remove:
self.game.db.flights.remove(fid) self.game.db.flights.remove(fid)
@ -156,6 +162,7 @@ class Migrator:
try_set_attr(s, "primary_task", preferred_task) try_set_attr(s, "primary_task", preferred_task)
try_set_attr(s, "max_size", 12) try_set_attr(s, "max_size", 12)
try_set_attr(s, "radio_presets", {}) try_set_attr(s, "radio_presets", {})
try_set_attr(s, "livery_set", [])
if isinstance(s.country, str): if isinstance(s.country, str):
c = country_dict.get(s.country, s.country) c = country_dict.get(s.country, s.country)
s.country = countries_by_name[c]() s.country = countries_by_name[c]()
@ -201,12 +208,6 @@ class Migrator:
c.faction.air_defense_units = set(c.faction.air_defense_units) c.faction.air_defense_units = set(c.faction.air_defense_units)
if isinstance(c.faction.missiles, list): if isinstance(c.faction.missiles, list):
c.faction.missiles = set(c.faction.missiles) c.faction.missiles = set(c.faction.missiles)
if isinstance(c.faction.carrier_names, list):
c.faction.carrier_names = set(c.faction.carrier_names)
if isinstance(c.faction.helicopter_carrier_names, list):
c.faction.helicopter_carrier_names = set(
c.faction.helicopter_carrier_names
)
if isinstance(c.faction.naval_units, list): if isinstance(c.faction.naval_units, list):
c.faction.naval_units = set(c.faction.naval_units) c.faction.naval_units = set(c.faction.naval_units)
if isinstance(c.faction.building_set, list): if isinstance(c.faction.building_set, list):

View File

@ -1,6 +1,7 @@
import logging import logging
from typing import Any, Optional, Type, List from typing import Any, Optional, Type, List
from dcs.point import MovingPoint
from dcs.task import ( from dcs.task import (
AWACS, AWACS,
AWACSTaskAction, AWACSTaskAction,
@ -31,8 +32,10 @@ from dcs.unitgroup import FlyingGroup
from game.ato import Flight, FlightType from game.ato import Flight, FlightType
from game.ato.flightplans.aewc import AewcFlightPlan from game.ato.flightplans.aewc import AewcFlightPlan
from game.ato.flightplans.formationattack import FormationAttackLayout
from game.ato.flightplans.packagerefueling import PackageRefuelingFlightPlan from game.ato.flightplans.packagerefueling import PackageRefuelingFlightPlan
from game.ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan from game.ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan
from game.utils import nautical_miles
class AircraftBehavior: class AircraftBehavior:
@ -100,8 +103,25 @@ class AircraftBehavior:
flight.squadron.coalition.game.settings.ai_unlimited_fuel flight.squadron.coalition.game.settings.ai_unlimited_fuel
) )
# at IP, insert waypoint to orient aircraft in correct direction
layout = flight.flight_plan.layout
at_ip_or_combat = flight.state.is_at_ip or flight.state.in_combat
if at_ip_or_combat and isinstance(layout, FormationAttackLayout):
a = group.points[0].position
b = layout.targets[0].position
pos = a.point_from_heading(
a.heading_between_point(b), nautical_miles(1).meters
)
point = MovingPoint(pos)
point.alt = group.points[0].alt
point.alt_type = group.points[0].alt_type
point.ETA_locked = False
point.speed = group.points[0].speed
point.name = "Orientation WPT"
group.points.insert(1, point)
# Activate AI unlimited fuel for all flights at startup # Activate AI unlimited fuel for all flights at startup
if ai_unlimited_fuel and not (flight.state.is_at_ip or flight.state.in_combat): if ai_unlimited_fuel and not at_ip_or_combat:
group.points[0].tasks.append(SetUnlimitedFuelCommand(True)) group.points[0].tasks.append(SetUnlimitedFuelCommand(True))
group.points[0].tasks.append(OptReactOnThreat(react_on_threat)) group.points[0].tasks.append(OptReactOnThreat(react_on_threat))

View File

@ -27,7 +27,14 @@ class AircraftPainter:
def livery_from_squadron(self) -> Optional[str]: def livery_from_squadron(self) -> Optional[str]:
return self.flight.squadron.livery return self.flight.squadron.livery
def livery_from_squadron_set(self) -> Optional[str]:
if not self.flight.squadron.livery_set:
return None
return random.choice(self.flight.squadron.livery_set)
def determine_livery(self) -> Optional[str]: def determine_livery(self) -> Optional[str]:
if (livery := self.livery_from_squadron_set()) is not None:
return livery
if (livery := self.livery_from_squadron()) is not None: if (livery := self.livery_from_squadron()) is not None:
return livery return livery
if (livery := self.livery_from_faction()) is not None: if (livery := self.livery_from_faction()) is not None:
@ -37,8 +44,10 @@ class AircraftPainter:
return None return None
def apply_livery(self) -> None: def apply_livery(self) -> None:
for unit, member in zip(self.group.units, self.flight.iter_members()):
livery = self.determine_livery() livery = self.determine_livery()
if livery is None: if not (livery or member.livery):
return continue
for unit in self.group.units: unit.livery_id = member.livery if member.livery else livery
unit.livery_id = livery assert isinstance(unit.livery_id, str)
unit.livery_id = unit.livery_id.lower()

View File

@ -1,6 +1,6 @@
import logging import logging
import random import random
from typing import Any, Union, Tuple, Optional from typing import Any, Union, Tuple, Optional, List
from dcs import Mission from dcs import Mission
from dcs.country import Country from dcs.country import Country
@ -19,7 +19,8 @@ from dcs.planes import (
) )
from dcs.point import PointAction from dcs.point import PointAction
from dcs.ships import KUZNECOW from dcs.ships import KUZNECOW
from dcs.terrain import NoParkingSlotError from dcs.terrain import NoParkingSlotError, Sinai, ParkingSlot
from dcs.terrain.sinai.airports import Nevatim
from dcs.unitgroup import ( from dcs.unitgroup import (
FlyingGroup, FlyingGroup,
ShipGroup, ShipGroup,
@ -110,24 +111,31 @@ class FlightGroupSpawner:
def create_idle_aircraft(self) -> Optional[FlyingGroup[Any]]: def create_idle_aircraft(self) -> Optional[FlyingGroup[Any]]:
group = None group = None
if ( cp = self.flight.squadron.location
self.flight.is_helo if self.flight.is_helo or self.flight.is_lha and isinstance(cp, Fob):
or self.flight.is_lha
and isinstance(self.flight.squadron.location, Fob)
):
group = self._generate_at_cp_helipad( group = self._generate_at_cp_helipad(
name=namegen.next_aircraft_name(self.country, self.flight), name=namegen.next_aircraft_name(self.country, self.flight),
cp=self.flight.squadron.location, cp=self.flight.squadron.location,
) )
elif isinstance(self.flight.squadron.location, Fob): elif isinstance(cp, Fob):
group = self._generate_at_cp_ground_spawn( group = self._generate_at_cp_ground_spawn(
name=namegen.next_aircraft_name(self.country, self.flight), name=namegen.next_aircraft_name(self.country, self.flight),
cp=self.flight.squadron.location, cp=self.flight.squadron.location,
) )
elif isinstance(self.flight.squadron.location, Airfield): elif isinstance(cp, Airfield):
# TODO: remove hack when fixed in DCS
slots = None
if self._check_nevatim_hack(cp):
ac_type = self.flight.unit_type.dcs_unit_type
slots = [
slot
for slot in cp.dcs_airport.free_parking_slots(ac_type)
if slot.slot_name in [str(n) for n in range(55, 66)]
]
group = self._generate_at_airfield( group = self._generate_at_airfield(
name=namegen.next_aircraft_name(self.country, self.flight), name=namegen.next_aircraft_name(self.country, self.flight),
airfield=self.flight.squadron.location, airfield=cp,
parking_slots=slots,
) )
if group: if group:
group.uncontrolled = True group.uncontrolled = True
@ -196,6 +204,18 @@ class FlightGroupSpawner:
pad_group = self._generate_at_cp_ground_spawn(name, cp) pad_group = self._generate_at_cp_ground_spawn(name, cp)
if pad_group is not None: if pad_group is not None:
return pad_group return pad_group
# TODO: get rid of the nevatim hack once fixed in DCS...
if self._check_nevatim_hack(cp):
slots = [
slot
for slot in cp.dcs_airport.free_parking_slots(
self.flight.squadron.aircraft.dcs_unit_type
)
if slot.slot_name in [str(n) for n in range(55, 66)]
]
return self._generate_at_airfield(name, cp, slots)
else:
return self._generate_at_airfield(name, cp) return self._generate_at_airfield(name, cp)
else: else:
raise NotImplementedError( raise NotImplementedError(
@ -210,6 +230,13 @@ class FlightGroupSpawner:
group = self._generate_over_departure(name, cp) group = self._generate_over_departure(name, cp)
return group return group
def _check_nevatim_hack(self, cp: ControlPoint) -> bool:
# TODO: get rid of the nevatim hack once fixed in DCS...
nevatim_hack = self.flight.coalition.game.settings.nevatim_parking_fix
nevatim_hack &= isinstance(self.mission.terrain, Sinai)
nevatim_hack &= isinstance(cp.dcs_airport, Nevatim)
return nevatim_hack
def generate_mid_mission(self) -> FlyingGroup[Any]: def generate_mid_mission(self) -> FlyingGroup[Any]:
assert isinstance(self.flight.state, InFlight) assert isinstance(self.flight.state, InFlight)
name = namegen.next_aircraft_name(self.country, self.flight) name = namegen.next_aircraft_name(self.country, self.flight)
@ -251,7 +278,12 @@ class FlightGroupSpawner:
group.points[0].alt_type = alt_type group.points[0].alt_type = alt_type
return group return group
def _generate_at_airfield(self, name: str, airfield: Airfield) -> FlyingGroup[Any]: def _generate_at_airfield(
self,
name: str,
airfield: Airfield,
parking_slots: Optional[List[ParkingSlot]] = None,
) -> FlyingGroup[Any]:
# TODO: Delayed runway starts should be converted to air starts for multiplayer. # TODO: Delayed runway starts should be converted to air starts for multiplayer.
# Runway starts do not work with late activated aircraft in multiplayer. Instead # Runway starts do not work with late activated aircraft in multiplayer. Instead
# of spawning on the runway the aircraft will spawn on the taxiway, potentially # of spawning on the runway the aircraft will spawn on the taxiway, potentially
@ -268,7 +300,7 @@ class FlightGroupSpawner:
maintask=None, maintask=None,
start_type=self._start_type_at_airfield(airfield), start_type=self._start_type_at_airfield(airfield),
group_size=self.flight.count, group_size=self.flight.count,
parking_slots=None, parking_slots=parking_slots,
callsign_name=self.flight.callsign.name if self.flight.callsign else None, callsign_name=self.flight.callsign.name if self.flight.callsign else None,
callsign_nr=self.flight.callsign.nr if self.flight.callsign else None, callsign_nr=self.flight.callsign.nr if self.flight.callsign else None,
) )

View File

@ -1,7 +1,7 @@
from dcs.point import MovingPoint from dcs.point import MovingPoint
from dcs.task import Expend, WeaponType, CarpetBombing, OptROE from dcs.task import Expend, WeaponType, CarpetBombing
from game.ato.flightwaypointtype import FlightWaypointType from game.ato.flightplans.airassault import AirAssaultLayout
from game.utils import feet, knots from game.utils import feet, knots
from pydcs_extensions.hercules.hercules import Hercules from pydcs_extensions.hercules.hercules import Hercules
from .pydcswaypointbuilder import PydcsWaypointBuilder from .pydcswaypointbuilder import PydcsWaypointBuilder
@ -12,14 +12,14 @@ class AirAssaultIngressBuilder(PydcsWaypointBuilder):
self.register_special_ingress_points() self.register_special_ingress_points()
air_drop = self.group.units[0].unit_type in [Hercules] air_drop = self.group.units[0].unit_type in [Hercules]
if air_drop: if air_drop:
waypoint.speed = knots(230).meters_per_second waypoint.speed = knots(200).meters_per_second
waypoint.speed_locked = True waypoint.speed_locked = True
waypoint.ETA_locked = False waypoint.ETA_locked = False
tgt = self.flight.flight_plan.package.target.position tgt = self.flight.package.target.position
for wpt in self.flight.flight_plan.waypoints: layout = self.flight.flight_plan.layout
if wpt.waypoint_type == FlightWaypointType.TARGET_GROUP_LOC: assert isinstance(layout, AirAssaultLayout)
tgt = wpt.position heading = layout.ingress.position.heading_between_point(tgt)
break tgt = tgt.point_from_heading(heading, feet(6000).meters)
bombing = CarpetBombing( bombing = CarpetBombing(
tgt, tgt,
weapon_type=WeaponType.Bombs, weapon_type=WeaponType.Bombs,

View File

@ -44,7 +44,16 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
task = AttackGroup( task = AttackGroup(
miz_group.id, miz_group.id,
weapon_type=WeaponType.Guided, weapon_type=WeaponType.ASM,
expend=Expend.All,
altitude=waypoint.alt,
group_attack=True,
)
waypoint.tasks.append(task)
task = AttackGroup(
miz_group.id,
weapon_type=WeaponType.GuidedBombs,
altitude=waypoint.alt, altitude=waypoint.alt,
) )
waypoint.tasks.append(task) waypoint.tasks.append(task)

View File

@ -40,4 +40,4 @@ class HoldPointBuilder(PydcsWaypointBuilder):
if self.flight.is_helo: if self.flight.is_helo:
waypoint.add_task(OptFormation.rotary_column()) waypoint.add_task(OptFormation.rotary_column())
else: else:
waypoint.add_task(OptFormation.finger_four_close()) waypoint.add_task(OptFormation.finger_four_open())

View File

@ -8,9 +8,7 @@ class LandingPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint: def build(self) -> MovingPoint:
waypoint = super().build() waypoint = super().build()
if self.ai_despawn(waypoint): if self.ai_despawn(waypoint):
waypoint.alt = round( waypoint.alt = round(self.flight.unit_type.preferred_patrol_altitude.meters)
self.flight.coalition.doctrine.max_patrol_altitude.meters
)
waypoint.alt_type = "BARO" waypoint.alt_type = "BARO"
else: else:
waypoint.type = "Land" waypoint.type = "Land"

View File

@ -124,7 +124,9 @@ class PydcsWaypointBuilder:
return False return False
def register_special_strike_points( def register_special_strike_points(
self, targets: Iterable[Union[MissionTarget, TheaterUnit]] self,
targets: Iterable[Union[MissionTarget, TheaterUnit]],
start: int = 1,
) -> None: ) -> None:
"""Create special strike waypoints for various aircraft""" """Create special strike waypoints for various aircraft"""
for i, t in enumerate(targets): for i, t in enumerate(targets):
@ -135,7 +137,7 @@ class PydcsWaypointBuilder:
# Add F-15E mission target points as mission 1 (for JDAM for instance) # Add F-15E mission target points as mission 1 (for JDAM for instance)
if self.group.units[0].unit_type == F_15ESE: if self.group.units[0].unit_type == F_15ESE:
self.group.add_nav_target_point( self.group.add_nav_target_point(
t.position, f"M{(i//8)+1}.{i%8+1}" f"\nH-1" f"\nA0" f"\nV0" t.position, f"M{(i//8)+start}.{i%8+1}\nH-1\nA0\nV0"
) )
def register_special_ingress_points(self) -> None: def register_special_ingress_points(self) -> None:

View File

@ -29,7 +29,7 @@ class SplitPointBuilder(PydcsWaypointBuilder):
if self.flight.is_helo: if self.flight.is_helo:
waypoint.tasks.append(OptFormation.rotary_wedge()) waypoint.tasks.append(OptFormation.rotary_wedge())
else: else:
waypoint.tasks.append(OptFormation.finger_four_close()) waypoint.tasks.append(OptFormation.finger_four_open())
waypoint.speed_locked = True waypoint.speed_locked = True
waypoint.ETA_locked = False waypoint.ETA_locked = False
if self.flight.is_helo: if self.flight.is_helo:

View File

@ -2,7 +2,7 @@ import copy
from typing import Union from typing import Union
from dcs import Point from dcs import Point
from dcs.planes import B_17G, B_52H, Tu_22M3, B_1B from dcs.planes import B_17G, B_52H, Tu_22M3, B_1B, F_15ESE
from dcs.point import MovingPoint from dcs.point import MovingPoint
from dcs.task import Bombing, Expend, OptFormation, WeaponType, CarpetBombing from dcs.task import Bombing, Expend, OptFormation, WeaponType, CarpetBombing
@ -62,13 +62,31 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
def add_strike_tasks( def add_strike_tasks(
self, waypoint: MovingPoint, weapon_type: WeaponType = WeaponType.Auto self, waypoint: MovingPoint, weapon_type: WeaponType = WeaponType.Auto
) -> None: ) -> None:
bomber = self.group.units[0].unit_type in [B_1B, B_52H]
ratio = len(self.group.units) / len(self.waypoint.targets)
for target in self.waypoint.targets: for target in self.waypoint.targets:
bombing = Bombing(target.position, weapon_type=weapon_type) bombing = Bombing(target.position, weapon_type=weapon_type)
# If there is only one target, drop all ordnance in one pass. # If there is only one target, drop all ordnance in one pass with group attack.
if len(self.waypoint.targets) == 1: if len(self.waypoint.targets) == 1:
bombing.params["expend"] = Expend.All.value bombing.params["expend"] = Expend.All.value
elif target.is_static: bombing.params["groupAttack"] = True
elif ratio >= 1:
# #TGTs > 1 & #AC >= #TGTs => each AC drops entire payload per TGT
bombing.params["expend"] = Expend.All.value
elif 1 > ratio >= 0.5:
# #TGTs > 1 & 2 * #AC >= #TGTs => each AC drops half payload per TGT
bombing.params["expend"] = Expend.Half.value bombing.params["expend"] = Expend.Half.value
elif 0.5 > ratio >= 0.25:
# #TGTs > 1 & 4 * #AC >= #TGTs => each AC drops quarter payload per TGT
bombing.params["expend"] = Expend.Quarter.value
elif 0.25 > ratio >= (1.0 / 6) and bomber:
# #TGTs > 1 & 4 * #AC < #TGTs & bomber => each AC drops 4 bombs per TGT
bombing.params["expend"] = Expend.Four.value
elif bomber:
# #TGTs > 1 & 6 * #AC < #TGTs & bomber => each AC drops 2 bombs per TGT
bombing.params["expend"] = Expend.Two.value
# else => Auto QTY
waypoint.tasks.append(bombing) waypoint.tasks.append(bombing)
waypoint.speed = mach(0.85, meters(waypoint.alt)).meters_per_second waypoint.speed = mach(0.85, meters(waypoint.alt)).meters_per_second
@ -76,4 +94,6 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
# Register special waypoints # Register special waypoints
if not self._special_wpts_injected: if not self._special_wpts_injected:
self.register_special_strike_points(self.waypoint.targets) self.register_special_strike_points(self.waypoint.targets)
if self.flight.unit_type.dcs_unit_type == F_15ESE:
self.register_special_strike_points(self.flight.custom_targets, 2)
self._special_wpts_injected = True self._special_wpts_injected = True

View File

@ -12,7 +12,7 @@ from dcs.task import StartCommand
from dcs.triggers import Event, TriggerOnce, TriggerRule from dcs.triggers import Event, TriggerOnce, TriggerRule
from dcs.unitgroup import FlyingGroup from dcs.unitgroup import FlyingGroup
from game.ato import Flight, FlightWaypoint from game.ato import Flight, FlightWaypoint, FlightType
from game.ato.flightstate import InFlight, WaitingForStart from game.ato.flightstate import InFlight, WaitingForStart
from game.ato.flightwaypointtype import FlightWaypointType from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.starttype import StartType from game.ato.starttype import StartType
@ -73,6 +73,18 @@ class WaypointGenerator:
if point.only_for_player and not self.flight.client_count: if point.only_for_player and not self.flight.client_count:
continue continue
if isinstance(self.flight.state, InFlight): if isinstance(self.flight.state, InFlight):
if self.flight.flight_type in [
FlightType.ESCORT,
FlightType.SEAD_ESCORT,
]:
is_join = point.waypoint_type == FlightWaypointType.JOIN
join_passed = self.flight.state.has_passed_waypoint(point)
if (
is_join
and join_passed
and point != self.flight.state.current_waypoint
):
self.builder_for_waypoint(point).add_tasks(self.group.points[0])
if point == self.flight.state.current_waypoint: if point == self.flight.state.current_waypoint:
# We don't need to build this waypoint because pydcs did that for # We don't need to build this waypoint because pydcs did that for
# us, but we do need to configure the tasks for it so that mid- # us, but we do need to configure the tasks for it so that mid-

View File

@ -50,6 +50,7 @@ from game.weather.weather import Weather
from .aircraft.flightdata import FlightData from .aircraft.flightdata import FlightData
from .briefinggenerator import CommInfo, JtacInfo, MissionInfoGenerator from .briefinggenerator import CommInfo, JtacInfo, MissionInfoGenerator
from .missiondata import AwacsInfo, TankerInfo from .missiondata import AwacsInfo, TankerInfo
from ..persistency import kneeboards_dir
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@ -440,13 +441,14 @@ class BriefingPage(KneeboardPage):
sun = Sun(start_pos.lat, start_pos.lng) sun = Sun(start_pos.lat, start_pos.lng)
date = fl.squadron.coalition.game.date date = fl.squadron.coalition.game.date
dt = datetime.datetime(date.year, date.month, date.day)
tz = fl.squadron.coalition.game.theater.timezone tz = fl.squadron.coalition.game.theater.timezone
# Get today's sunrise and sunset in UTC # Get today's sunrise and sunset in UTC
sr_utc = sun.get_sunrise_time(date) sr_utc = sun.get_sunrise_time(dt)
ss_utc = sun.get_sunset_time(date) ss_utc = sun.get_sunset_time(dt)
sr = sr_utc + tz.utcoffset(sun.get_sunrise_time(date)) sr = sr_utc + tz.utcoffset(sun.get_sunrise_time(dt))
ss = ss_utc + tz.utcoffset(sun.get_sunset_time(date)) ss = ss_utc + tz.utcoffset(sun.get_sunset_time(dt))
writer.text( writer.text(
f"Sunrise - Sunset: {sr.strftime('%H:%M')} - {ss.strftime('%H:%M')}" f"Sunrise - Sunset: {sr.strftime('%H:%M')} - {ss.strftime('%H:%M')}"
@ -820,6 +822,14 @@ class KneeboardGenerator(MissionInfoGenerator):
page_path = aircraft_dir / f"page{idx:02}.png" page_path = aircraft_dir / f"page{idx:02}.png"
page.write(page_path) page.write(page_path)
self.mission.add_aircraft_kneeboard(aircraft.dcs_unit_type, page_path) self.mission.add_aircraft_kneeboard(aircraft.dcs_unit_type, page_path)
if not kneeboards_dir().exists():
return
for type in kneeboards_dir().iterdir():
if type.is_dir():
for kneeboard in type.iterdir():
self.mission.custom_kneeboards[type.name].append(kneeboard)
else:
self.mission.custom_kneeboards[""].append(type)
def pages_by_airframe(self) -> Dict[AircraftType, List[KneeboardPage]]: def pages_by_airframe(self) -> Dict[AircraftType, List[KneeboardPage]]:
"""Returns a list of kneeboard pages per airframe in the mission. """Returns a list of kneeboard pages per airframe in the mission.

View File

@ -39,7 +39,13 @@ from dcs.task import (
OptAlarmState, OptAlarmState,
) )
from dcs.translation import String from dcs.translation import String
from dcs.triggers import Event, TriggerOnce, TriggerStart, TriggerZone from dcs.triggers import (
Event,
TriggerOnce,
TriggerStart,
TriggerZone,
TriggerZoneQuadPoint,
)
from dcs.unit import Unit, InvisibleFARP, BaseFARP, SingleHeliPad, FARP from dcs.unit import Unit, InvisibleFARP, BaseFARP, SingleHeliPad, FARP
from dcs.unitgroup import MovingGroup, ShipGroup, StaticGroup, VehicleGroup from dcs.unitgroup import MovingGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import ShipType, VehicleType from dcs.unittype import ShipType, VehicleType
@ -404,6 +410,16 @@ class GroundObjectGenerator:
# is minimized. As long as the triggerzone is over the scenery object, we're ok. # is minimized. As long as the triggerzone is over the scenery object, we're ok.
smallest_valid_radius = feet(16).meters smallest_valid_radius = feet(16).meters
if isinstance(scenery.zone, TriggerZoneQuadPoint):
trigger_zone: TriggerZone = self.m.triggers.add_triggerzone_quad(
scenery.zone.position,
scenery.zone.verticies,
scenery.zone.hidden,
scenery.zone.name,
color,
scenery.zone.properties,
)
else:
trigger_zone = self.m.triggers.add_triggerzone( trigger_zone = self.m.triggers.add_triggerzone(
scenery.zone.position, scenery.zone.position,
smallest_valid_radius, smallest_valid_radius,

View File

@ -111,6 +111,10 @@ def airwing_dir() -> Path:
return base_path() / "Retribution" / "AirWing" return base_path() / "Retribution" / "AirWing"
def kneeboards_dir() -> Path:
return base_path() / "Retribution" / "Kneeboards"
def payloads_dir(backup: bool = False) -> Path: def payloads_dir(backup: bool = False) -> Path:
payloads = base_path() / "MissionEditor" / "UnitPayloads" payloads = base_path() / "MissionEditor" / "UnitPayloads"
if backup: if backup:

View File

@ -1,4 +1,4 @@
from asyncio import wait from asyncio import wait, tasks
from fastapi import APIRouter, WebSocket from fastapi import APIRouter, WebSocket
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
@ -17,7 +17,7 @@ class ConnectionManager:
async def shutdown(self) -> None: async def shutdown(self) -> None:
futures = [] futures = []
for connection in self.active_connections: for connection in self.active_connections:
futures.append(connection.close()) futures.append(tasks.create_task(connection.close()))
await wait(futures) await wait(futures)
async def connect(self, websocket: WebSocket) -> None: async def connect(self, websocket: WebSocket) -> None:
@ -30,7 +30,9 @@ class ConnectionManager:
async def broadcast(self, events: GameUpdateEventsJs) -> None: async def broadcast(self, events: GameUpdateEventsJs) -> None:
futures = [] futures = []
for connection in self.active_connections: for connection in self.active_connections:
futures.append(connection.send_json(jsonable_encoder(events))) futures.append(
tasks.create_task(connection.send_json(jsonable_encoder(events)))
)
await wait(futures) await wait(futures)

View File

@ -314,8 +314,18 @@ class Settings:
page=CAMPAIGN_DOCTRINE_PAGE, page=CAMPAIGN_DOCTRINE_PAGE,
section=GENERAL_SECTION, section=GENERAL_SECTION,
default=False, default=False,
detail=("AI will jettison their fuel tanks as soon as they're empty."), detail="AI will jettison their fuel tanks as soon as they're empty.",
) )
max_plane_altitude_offset: int = bounded_int_option(
"Maximum randomized altitude offset (x1000 ft) for airplanes.",
page=CAMPAIGN_DOCTRINE_PAGE,
section=GENERAL_SECTION,
min=0,
max=5,
default=2,
detail="Creates a randomized altitude offset for airplanes.",
)
# Doctrine Distances Section
airbase_threat_range: int = bounded_int_option( airbase_threat_range: int = bounded_int_option(
"Airbase threat range (nmi)", "Airbase threat range (nmi)",
page=CAMPAIGN_DOCTRINE_PAGE, page=CAMPAIGN_DOCTRINE_PAGE,
@ -388,6 +398,32 @@ class Settings:
"planned to known threat zones." "planned to known threat zones."
), ),
) )
max_mission_range_planes: int = bounded_int_option(
"Auto-planner maximum mission range for airplanes (NM)",
page=CAMPAIGN_DOCTRINE_PAGE,
section=DOCTRINE_DISTANCES_SECTION,
default=150,
min=150,
max=1000,
detail=(
"The maximum mission distance that's used by the auto-planner for airplanes. "
"This setting won't take effect when a larger "
"range is defined in the airplane's yaml specification."
),
)
max_mission_range_helicopters: int = bounded_int_option(
"Auto-planner maximum mission range for helicopters (NM)",
page=CAMPAIGN_DOCTRINE_PAGE,
section=DOCTRINE_DISTANCES_SECTION,
default=100,
min=50,
max=1000,
detail=(
"The maximum mission distance that's used by the auto-planner for helicopters. "
"This setting won't take effect when a larger "
"range is defined in the helicopter's yaml specification."
),
)
# Pilots and Squadrons # Pilots and Squadrons
ai_pilot_levelling: bool = boolean_option( ai_pilot_levelling: bool = boolean_option(
"Allow AI pilot leveling", "Allow AI pilot leveling",
@ -720,6 +756,16 @@ class Settings:
"will not be included in automatically planned OCA packages." "will not be included in automatically planned OCA packages."
), ),
) )
nevatim_parking_fix: bool = boolean_option(
"Force air-starts for all aircraft at Nevatim",
page=MISSION_GENERATOR_PAGE,
section=GAMEPLAY_SECTION,
default=True, # TODO: set to False or remove this when DCS is fixed
detail=(
"Air-starts forced for all aircraft at Nevatim except parking slots "
"55 till 65, since those are the only ones that work."
),
)
# Mission specific # Mission specific
desired_player_mission_duration: timedelta = minutes_option( desired_player_mission_duration: timedelta = minutes_option(
"Desired mission duration", "Desired mission duration",
@ -1150,6 +1196,7 @@ class Settings:
enable_transfer_cheat: bool = False enable_transfer_cheat: bool = False
enable_runway_state_cheat: bool = False enable_runway_state_cheat: bool = False
enable_air_wing_adjustments: bool = False enable_air_wing_adjustments: bool = False
enable_enemy_buy_sell: bool = False
# LUA Plugins system # LUA Plugins system
plugins: Dict[str, bool] = field(default_factory=dict) plugins: Dict[str, bool] = field(default_factory=dict)

View File

@ -52,6 +52,7 @@ class AirWing:
heli: bool, heli: bool,
this_turn: bool, this_turn: bool,
preferred_type: Optional[AircraftType] = None, preferred_type: Optional[AircraftType] = None,
ignore_range: bool = False,
) -> list[Squadron]: ) -> list[Squadron]:
airfield_cache = ObjectiveDistanceCache.get_closest_airfields(location) airfield_cache = ObjectiveDistanceCache.get_closest_airfields(location)
best_aircraft = AircraftType.priority_list_for_task(task) best_aircraft = AircraftType.priority_list_for_task(task)
@ -68,7 +69,7 @@ class AirWing:
] ]
for squadron in squadrons: for squadron in squadrons:
if squadron.can_auto_assign_mission( if squadron.can_auto_assign_mission(
location, task, size, heli, this_turn location, task, size, heli, this_turn, ignore_range
): ):
capable_at_base.append(squadron) capable_at_base.append(squadron)
if squadron.aircraft not in best_aircraft: if squadron.aircraft not in best_aircraft:
@ -100,9 +101,10 @@ class AirWing:
heli: bool, heli: bool,
this_turn: bool, this_turn: bool,
preferred_type: Optional[AircraftType] = None, preferred_type: Optional[AircraftType] = None,
ignore_range: bool = False,
) -> Optional[Squadron]: ) -> Optional[Squadron]:
for squadron in self.best_squadrons_for( for squadron in self.best_squadrons_for(
location, task, size, heli, this_turn, preferred_type location, task, size, heli, this_turn, preferred_type, ignore_range
): ):
return squadron return squadron
return None return None

View File

@ -18,7 +18,7 @@ from game.theater import ParkingType
from .pilot import Pilot, PilotStatus from .pilot import Pilot, PilotStatus
from ..db.database import Database from ..db.database import Database
from ..radio.radios import RadioFrequency from ..radio.radios import RadioFrequency
from ..utils import meters from ..utils import meters, nautical_miles
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@ -40,6 +40,7 @@ class Squadron:
aircraft: AircraftType aircraft: AircraftType
max_size: int max_size: int
livery: Optional[str] livery: Optional[str]
livery_set: list[str] # will override livery if not empty
primary_task: FlightType primary_task: FlightType
auto_assignable_mission_types: set[FlightType] auto_assignable_mission_types: set[FlightType]
radio_presets: dict[Union[str, int], list[RadioFrequency]] radio_presets: dict[Union[str, int], list[RadioFrequency]]
@ -281,6 +282,7 @@ class Squadron:
size: int, size: int,
heli: bool, heli: bool,
this_turn: bool, this_turn: bool,
ignore_range: bool = False,
) -> bool: ) -> bool:
if ( if (
self.location.cptype.name in ["FOB", "FARP"] self.location.cptype.name in ["FOB", "FARP"]
@ -304,8 +306,23 @@ class Squadron:
if heli and task == FlightType.REFUELING: if heli and task == FlightType.REFUELING:
return False return False
if ignore_range:
return True
distance_to_target = meters(location.distance_to(self.location)) distance_to_target = meters(location.distance_to(self.location))
return distance_to_target <= self.aircraft.max_mission_range max_plane_dist = nautical_miles(
self.coalition.game.settings.max_mission_range_planes
)
max_heli_dist = nautical_miles(
self.coalition.game.settings.max_mission_range_helicopters
)
if self.aircraft.helicopter:
return distance_to_target <= max(
self.aircraft.max_mission_range, max_heli_dist
)
return distance_to_target <= max(
self.aircraft.max_mission_range, max_plane_dist
)
def operates_from(self, control_point: ControlPoint) -> bool: def operates_from(self, control_point: ControlPoint) -> bool:
if not control_point.can_operate(self.aircraft): if not control_point.can_operate(self.aircraft):
@ -487,6 +504,7 @@ class Squadron:
squadron_def.aircraft, squadron_def.aircraft,
max_size, max_size,
squadron_def.livery, squadron_def.livery,
squadron_def.livery_set,
primary_task, primary_task,
squadron_def.auto_assignable_mission_types, squadron_def.auto_assignable_mission_types,
squadron_def.radio_presets, squadron_def.radio_presets,

View File

@ -27,6 +27,7 @@ class SquadronDef:
role: str role: str
aircraft: AircraftType aircraft: AircraftType
livery: Optional[str] livery: Optional[str]
livery_set: list[str]
auto_assignable_mission_types: set[FlightType] auto_assignable_mission_types: set[FlightType]
radio_presets: dict[Union[str, int], list[RadioFrequency]] radio_presets: dict[Union[str, int], list[RadioFrequency]]
operating_bases: OperatingBases operating_bases: OperatingBases
@ -103,6 +104,7 @@ class SquadronDef:
role=data["role"], role=data["role"],
aircraft=unit_type, aircraft=unit_type,
livery=data.get("livery"), livery=data.get("livery"),
livery_set=data.get("livery_set", []),
auto_assignable_mission_types=set(unit_type.iter_task_capabilities()), auto_assignable_mission_types=set(unit_type.iter_task_capabilities()),
radio_presets=radio_presets, radio_presets=radio_presets,
operating_bases=OperatingBases.from_yaml(unit_type, data.get("bases", {})), operating_bases=OperatingBases.from_yaml(unit_type, data.get("bases", {})),

View File

@ -1021,6 +1021,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
# clear the ATO and replan the airlifts with the correct time. # clear the ATO and replan the airlifts with the correct time.
self.ground_unit_orders.process(game, game.conditions.start_time) self.ground_unit_orders.process(game, game.conditions.start_time)
self.release_parking_slots()
runway_status = self.runway_status runway_status = self.runway_status
if runway_status is not None: if runway_status is not None:
runway_status.process_turn() runway_status.process_turn()
@ -1393,6 +1395,11 @@ class NavalControlPoint(
L02, L02,
L52, L52,
L61, L61,
CV_1143_5,
CVN_71,
CVN_72,
CVN_73,
CVN_75,
]: ]:
return True return True
return False return False

View File

@ -130,6 +130,8 @@ class IadsNetwork:
# but if it does, we want to know because it's supposed to be impossible afaict # but if it does, we want to know because it's supposed to be impossible afaict
skynet_node = SkynetNode.from_group(node.group) skynet_node = SkynetNode.from_group(node.group)
for connection in node.connections.values(): for connection in node.connections.values():
if not any([x.alive for x in connection.units]):
continue
if connection.ground_object.is_friendly( if connection.ground_object.is_friendly(
skynet_node.player skynet_node.player
) and not game.iads_considerate_culling(connection.ground_object): ) and not game.iads_considerate_culling(connection.ground_object):

View File

@ -7,12 +7,13 @@ from datetime import datetime, time
from typing import List, Optional from typing import List, Optional
import dcs.statics import dcs.statics
from dcs.countries import country_dict
from game import Game from game import Game
from game.factions.faction import Faction from game.factions.faction import Faction
from game.naming import namegen from game.naming import namegen
from game.scenery_group import SceneryGroup from game.scenery_group import SceneryGroup
from game.theater import PointWithHeading, PresetLocation from game.theater import PointWithHeading, PresetLocation, NavalControlPoint
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
BuildingGroundObject, BuildingGroundObject,
IadsBuildingGroundObject, IadsBuildingGroundObject,
@ -26,12 +27,21 @@ from . import (
Fob, Fob,
OffMapSpawn, OffMapSpawn,
) )
from .theatergroup import IadsGroundGroup, IadsRole, SceneryUnit, TheaterGroup from .theatergroup import (
IadsGroundGroup,
IadsRole,
SceneryUnit,
TheaterGroup,
TheaterUnit,
)
from ..armedforces.armedforces import ArmedForces from ..armedforces.armedforces import ArmedForces
from ..armedforces.forcegroup import ForceGroup from ..armedforces.forcegroup import ForceGroup
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
from ..campaignloader.campaigncarrierconfig import CampaignCarrierConfig
from ..campaignloader.campaigngroundconfig import TgoConfig from ..campaignloader.campaigngroundconfig import TgoConfig
from ..data.groups import GroupTask from ..data.groups import GroupTask
from ..data.units import UnitClass
from ..dcs.shipunittype import ShipUnitType
from ..profiling import logged_duration from ..profiling import logged_duration
from ..settings import Settings from ..settings import Settings
@ -49,6 +59,7 @@ class GeneratorSettings:
no_player_navy: bool no_player_navy: bool
no_enemy_navy: bool no_enemy_navy: bool
tgo_config: TgoConfig tgo_config: TgoConfig
carrier_config: CampaignCarrierConfig
squadrons_start_full: bool squadrons_start_full: bool
@ -58,6 +69,7 @@ class ModSettings:
a6a_intruder: bool = False a6a_intruder: bool = False
a7e_corsair2: bool = False a7e_corsair2: bool = False
f4bc_phantom: bool = False f4bc_phantom: bool = False
f9f_panther: bool = False
f15d_baz: bool = False f15d_baz: bool = False
f_15_idf: bool = False f_15_idf: bool = False
f_16_idf: bool = False f_16_idf: bool = False
@ -72,6 +84,7 @@ class ModSettings:
irondome: bool = False irondome: bool = False
uh_60l: bool = False uh_60l: bool = False
jas39_gripen: bool = False jas39_gripen: bool = False
super_etendard: bool = False
su30_flanker_h: bool = False su30_flanker_h: bool = False
su57_felon: bool = False su57_felon: bool = False
frenchpack: bool = False frenchpack: bool = False
@ -125,11 +138,13 @@ class GameGenerator:
def should_remove_carrier(self, player: bool) -> bool: def should_remove_carrier(self, player: bool) -> bool:
faction = self.player if player else self.enemy faction = self.player if player else self.enemy
return self.generator_settings.no_carrier or not faction.carrier_names return self.generator_settings.no_carrier or not faction.carriers
def should_remove_lha(self, player: bool) -> bool: def should_remove_lha(self, player: bool) -> bool:
faction = self.player if player else self.enemy faction = self.player if player else self.enemy
return self.generator_settings.no_lha or not faction.helicopter_carrier_names return self.generator_settings.no_lha or not [
x for x in faction.carriers if x.unit_class == UnitClass.HELICOPTER_CARRIER
]
def prepare_theater(self) -> None: def prepare_theater(self) -> None:
to_remove: List[ControlPoint] = [] to_remove: List[ControlPoint] = []
@ -221,14 +236,63 @@ class GenericCarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
carrier = next(self.control_point.ground_objects[-1].units) carrier = next(self.control_point.ground_objects[-1].units)
carrier.name = carrier_name carrier.name = carrier_name
def apply_carrier_config(self) -> None:
assert isinstance(self.control_point, NavalControlPoint)
# If the campaign designer has specified a preferred name, use that
# Note that the preferred name needs to exist in the faction, so we
# don't end up with Kuznetsov carriers called CV-59 Forrestal
preferred_name = None
preferred_type = None
carrier_map = self.generator_settings.carrier_config.by_original_name
if ccfg := carrier_map.get(self.control_point.name):
preferred_name = ccfg.preferred_name
preferred_type = ccfg.preferred_type
carrier_unit = self.get_carrier_unit()
if preferred_type and preferred_type.dcs_unit_type in [
v
for k, v in country_dict[self.faction.country.id].Ship.__dict__.items() # type: ignore
if "__" not in k
]:
carrier_unit.type = preferred_type.dcs_unit_type
if preferred_name:
self.control_point.name = preferred_name
else:
carrier_type = preferred_type if preferred_type else carrier_unit.unit_type
assert isinstance(carrier_type, ShipUnitType)
# Otherwise pick randomly from the names specified for that particular carrier type
carrier_names = self.faction.carriers.get(carrier_type)
if carrier_names:
self.control_point.name = random.choice(list(carrier_names))
else:
self.control_point.name = carrier_type.display_name
carrier_unit.name = self.control_point.name
# Prevents duplicate carrier or LHA names in campaigns with more that one of either.
for carrier_type_key in self.faction.carriers:
for carrier_name in self.faction.carriers[carrier_type_key]:
if carrier_name == self.control_point.name:
self.faction.carriers[carrier_type_key].remove(
self.control_point.name
)
def get_carrier_unit(self) -> TheaterUnit:
carrier_go = [
go
for go in self.control_point.ground_objects
if go.category in ["CARRIER", "LHA"]
][0]
groups = [
g for g in carrier_go.groups if "Carrier" in g.name or "LHA" in g.name
]
return groups[0].units[0]
class CarrierGroundObjectGenerator(GenericCarrierGroundObjectGenerator): class CarrierGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
def generate(self) -> bool: def generate(self) -> bool:
if not super().generate(): if not super().generate():
return False return False
carrier_names = self.faction.carrier_names carriers = self.faction.carriers
if not carrier_names: if not carriers:
logging.info( logging.info(
f"Skipping generation of {self.control_point.name} because " f"Skipping generation of {self.control_point.name} because "
f"{self.faction_name} has no carriers" f"{self.faction_name} has no carriers"
@ -239,6 +303,7 @@ class CarrierGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
if not unit_group: if not unit_group:
logging.error(f"{self.faction_name} has no access to AircraftCarrier") logging.error(f"{self.faction_name} has no access to AircraftCarrier")
return False return False
self.generate_ground_object_from_group( self.generate_ground_object_from_group(
unit_group, unit_group,
PresetLocation( PresetLocation(
@ -248,7 +313,7 @@ class CarrierGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
), ),
GroupTask.AIRCRAFT_CARRIER, GroupTask.AIRCRAFT_CARRIER,
) )
self.update_carrier_name(random.choice(list(carrier_names))) self.apply_carrier_config()
return True return True
@ -257,8 +322,8 @@ class LhaGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
if not super().generate(): if not super().generate():
return False return False
lha_names = self.faction.helicopter_carrier_names lhas = self.faction.carriers
if not lha_names: if not lhas:
logging.info( logging.info(
f"Skipping generation of {self.control_point.name} because " f"Skipping generation of {self.control_point.name} because "
f"{self.faction_name} has no LHAs" f"{self.faction_name} has no LHAs"
@ -280,7 +345,7 @@ class LhaGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
), ),
GroupTask.HELICOPTER_CARRIER, GroupTask.HELICOPTER_CARRIER,
) )
self.update_carrier_name(random.choice(list(lha_names))) self.apply_carrier_config()
return True return True

View File

@ -2,7 +2,7 @@ from pathlib import Path
MAJOR_VERSION = 1 MAJOR_VERSION = 1
MINOR_VERSION = 3 MINOR_VERSION = 4
MICRO_VERSION = 0 MICRO_VERSION = 0

View File

@ -2,6 +2,7 @@ from .SWPack import *
from .a4ec import * from .a4ec import *
from .a7e import * from .a7e import *
from .a6a import * from .a6a import *
from .f9f import *
from .f100 import * from .f100 import *
from .f104 import * from .f104 import *
from .f105 import * from .f105 import *
@ -20,6 +21,7 @@ from .irondome import *
from .jas39 import * from .jas39 import *
from .ov10a import * from .ov10a import *
from .spanishnavypack import * from .spanishnavypack import *
from .super_etendard import *
from .su30 import * from .su30 import *
from .su57 import * from .su57 import *
from .swedishmilitaryassetspack import * from .swedishmilitaryassetspack import *

View File

@ -0,0 +1 @@
from .f9f import *

699
pydcs_extensions/f9f/f9f.py Normal file
View File

@ -0,0 +1,699 @@
from typing import Set
from dcs import task
from dcs.planes import PlaneType
from dcs.weapons_data import Weapons
from game.modsupport import planemod
@planemod
class VSN_F9F(PlaneType):
id = "VSN_F9F"
flyable = True
height = 3.73
width = 11.58
length = 11.84
fuel_max = 2310
max_speed = 961.2
category = "Interceptor" # {78EFB7A2-FD52-4b57-A6A6-3BF0E1D6555F}
radio_frequency = 127.5
livery_name = "VSN_F9F" # from type
class Pylon1:
Smoke_Generator___red_ = (1, Weapons.Smoke_Generator___red_)
Smoke_Generator___green_ = (1, Weapons.Smoke_Generator___green_)
Smoke_Generator___blue_ = (1, Weapons.Smoke_Generator___blue_)
Smoke_Generator___white_ = (1, Weapons.Smoke_Generator___white_)
Smoke_Generator___yellow_ = (1, Weapons.Smoke_Generator___yellow_)
Smoke_Generator___orange_ = (1, Weapons.Smoke_Generator___orange_)
class Pylon2:
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
2,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
2,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
2,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
2,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
2,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
2,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
2,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
2,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
AN_M64___500lb_GP_Bomb_LD_ = (2, Weapons.AN_M64___500lb_GP_Bomb_LD_)
Mk_82___500lb_GP_Bomb_LD = (2, Weapons.Mk_82___500lb_GP_Bomb_LD)
Mk_81___250lb_GP_Bomb_LD = (2, Weapons.Mk_81___250lb_GP_Bomb_LD)
HVAR__UnGd_Rkt = (2, Weapons.HVAR__UnGd_Rkt)
LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk1__HE = (
2,
Weapons.LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk1__HE,
)
LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT = (
2,
Weapons.LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT,
)
# ERRR <CLEAN>
class Pylon3:
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
3,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
3,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
3,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
3,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
3,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
3,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
3,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
3,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
AN_M64___500lb_GP_Bomb_LD_ = (3, Weapons.AN_M64___500lb_GP_Bomb_LD_)
Mk_82___500lb_GP_Bomb_LD = (3, Weapons.Mk_82___500lb_GP_Bomb_LD)
Mk_81___250lb_GP_Bomb_LD = (3, Weapons.Mk_81___250lb_GP_Bomb_LD)
HVAR__UnGd_Rkt = (3, Weapons.HVAR__UnGd_Rkt)
LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk1__HE = (
3,
Weapons.LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk1__HE,
)
LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT = (
3,
Weapons.LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT,
)
# ERRR <CLEAN>
class Pylon4:
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
4,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
4,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
4,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
4,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
4,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
4,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
4,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
4,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
4,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
4,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
4,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
4,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
4,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
4,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
4,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
4,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
AN_M64___500lb_GP_Bomb_LD_ = (4, Weapons.AN_M64___500lb_GP_Bomb_LD_)
Mk_82___500lb_GP_Bomb_LD = (4, Weapons.Mk_82___500lb_GP_Bomb_LD)
Mk_81___250lb_GP_Bomb_LD = (4, Weapons.Mk_81___250lb_GP_Bomb_LD)
HVAR__UnGd_Rkt = (4, Weapons.HVAR__UnGd_Rkt)
LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk1__HE = (
4,
Weapons.LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk1__HE,
)
LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT = (
4,
Weapons.LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT,
)
# ERRR <CLEAN>
class Pylon5:
AIM_9B_Sidewinder_IR_AAM = (5, Weapons.AIM_9B_Sidewinder_IR_AAM)
Mk_83___1000lb_GP_Bomb_LD = (5, Weapons.Mk_83___1000lb_GP_Bomb_LD)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
5,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
5,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
5,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
5,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
5,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
5,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
5,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
5,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
M117___750lb_GP_Bomb_LD = (5, Weapons.M117___750lb_GP_Bomb_LD)
AN_M64___500lb_GP_Bomb_LD_ = (5, Weapons.AN_M64___500lb_GP_Bomb_LD_)
Mk_82___500lb_GP_Bomb_LD = (5, Weapons.Mk_82___500lb_GP_Bomb_LD)
Mk_81___250lb_GP_Bomb_LD = (5, Weapons.Mk_81___250lb_GP_Bomb_LD)
HVAR__UnGd_Rkt = (5, Weapons.HVAR__UnGd_Rkt)
# ERRR <CLEAN>
Smoke_Generator___red_ = (5, Weapons.Smoke_Generator___red_)
Smoke_Generator___green_ = (5, Weapons.Smoke_Generator___green_)
Smoke_Generator___blue_ = (5, Weapons.Smoke_Generator___blue_)
Smoke_Generator___white_ = (5, Weapons.Smoke_Generator___white_)
Smoke_Generator___yellow_ = (5, Weapons.Smoke_Generator___yellow_)
Smoke_Generator___orange_ = (5, Weapons.Smoke_Generator___orange_)
class Pylon6:
Smoke_Generator___red_ = (6, Weapons.Smoke_Generator___red_)
Smoke_Generator___green_ = (6, Weapons.Smoke_Generator___green_)
Smoke_Generator___blue_ = (6, Weapons.Smoke_Generator___blue_)
Smoke_Generator___white_ = (6, Weapons.Smoke_Generator___white_)
Smoke_Generator___yellow_ = (6, Weapons.Smoke_Generator___yellow_)
Smoke_Generator___orange_ = (6, Weapons.Smoke_Generator___orange_)
class Pylon7:
AIM_9B_Sidewinder_IR_AAM = (7, Weapons.AIM_9B_Sidewinder_IR_AAM)
Mk_83___1000lb_GP_Bomb_LD = (7, Weapons.Mk_83___1000lb_GP_Bomb_LD)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
7,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
7,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
7,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
7,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
7,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
7,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
7,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
7,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
7,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
7,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
7,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
7,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
7,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
7,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
7,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
7,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
M117___750lb_GP_Bomb_LD = (7, Weapons.M117___750lb_GP_Bomb_LD)
AN_M64___500lb_GP_Bomb_LD_ = (7, Weapons.AN_M64___500lb_GP_Bomb_LD_)
Mk_82___500lb_GP_Bomb_LD = (7, Weapons.Mk_82___500lb_GP_Bomb_LD)
Mk_81___250lb_GP_Bomb_LD = (7, Weapons.Mk_81___250lb_GP_Bomb_LD)
HVAR__UnGd_Rkt = (7, Weapons.HVAR__UnGd_Rkt)
# ERRR <CLEAN>
Smoke_Generator___red_ = (7, Weapons.Smoke_Generator___red_)
Smoke_Generator___green_ = (7, Weapons.Smoke_Generator___green_)
Smoke_Generator___blue_ = (7, Weapons.Smoke_Generator___blue_)
Smoke_Generator___white_ = (7, Weapons.Smoke_Generator___white_)
Smoke_Generator___yellow_ = (7, Weapons.Smoke_Generator___yellow_)
Smoke_Generator___orange_ = (7, Weapons.Smoke_Generator___orange_)
class Pylon8:
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
8,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
8,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
8,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
8,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
8,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
8,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
8,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
8,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
8,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
8,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
8,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
8,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
8,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
8,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
8,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
8,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
AN_M64___500lb_GP_Bomb_LD_ = (8, Weapons.AN_M64___500lb_GP_Bomb_LD_)
Mk_82___500lb_GP_Bomb_LD = (8, Weapons.Mk_82___500lb_GP_Bomb_LD)
Mk_81___250lb_GP_Bomb_LD = (8, Weapons.Mk_81___250lb_GP_Bomb_LD)
HVAR__UnGd_Rkt = (8, Weapons.HVAR__UnGd_Rkt)
LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk1__HE = (
8,
Weapons.LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk1__HE,
)
LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT = (
8,
Weapons.LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT,
)
# ERRR <CLEAN>
class Pylon9:
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
9,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
9,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
9,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
9,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
9,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
9,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
9,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
9,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
9,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
9,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
9,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
9,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
9,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
9,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
9,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
9,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
AN_M64___500lb_GP_Bomb_LD_ = (9, Weapons.AN_M64___500lb_GP_Bomb_LD_)
Mk_82___500lb_GP_Bomb_LD = (9, Weapons.Mk_82___500lb_GP_Bomb_LD)
Mk_81___250lb_GP_Bomb_LD = (9, Weapons.Mk_81___250lb_GP_Bomb_LD)
HVAR__UnGd_Rkt = (9, Weapons.HVAR__UnGd_Rkt)
LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk1__HE = (
9,
Weapons.LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk1__HE,
)
LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT = (
9,
Weapons.LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT,
)
# ERRR <CLEAN>
class Pylon10:
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
10,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
10,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
10,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
10,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
10,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
10,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
10,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
10,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
10,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
10,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
10,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
10,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
10,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
10,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
10,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
10,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
AN_M64___500lb_GP_Bomb_LD_ = (10, Weapons.AN_M64___500lb_GP_Bomb_LD_)
Mk_82___500lb_GP_Bomb_LD = (10, Weapons.Mk_82___500lb_GP_Bomb_LD)
Mk_81___250lb_GP_Bomb_LD = (10, Weapons.Mk_81___250lb_GP_Bomb_LD)
HVAR__UnGd_Rkt = (10, Weapons.HVAR__UnGd_Rkt)
LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk1__HE = (
10,
Weapons.LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk1__HE,
)
LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT = (
10,
Weapons.LAU_3_pod___19_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT,
)
# ERRR <CLEAN>
class Pylon11:
Smoke_Generator___red_ = (11, Weapons.Smoke_Generator___red_)
Smoke_Generator___green_ = (11, Weapons.Smoke_Generator___green_)
Smoke_Generator___blue_ = (11, Weapons.Smoke_Generator___blue_)
Smoke_Generator___white_ = (11, Weapons.Smoke_Generator___white_)
Smoke_Generator___yellow_ = (11, Weapons.Smoke_Generator___yellow_)
Smoke_Generator___orange_ = (11, Weapons.Smoke_Generator___orange_)
pylons: Set[int] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
tasks = [
task.CAP,
task.CAS,
task.Escort,
task.FighterSweep,
task.GroundAttack,
task.Intercept,
task.AntishipStrike,
]
task_default = task.GroundAttack

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
from .super_etendard import *

View File

@ -0,0 +1,144 @@
from dcs import task
from dcs.planes import PlaneType
from dcs.weapons_data import Weapons
from game.modsupport import planemod
from pydcs_extensions.weapon_injector import inject_weapons
class WeaponsSEM:
Fuel_Tank_1100_Liter = {
"clsid": "{SEM1100_PTB}",
"name": "Fuel Tank 1100 Liter",
"weight": 1150,
}
Fuel_Tank_625_Liter = {
"clsid": "{SEM625_PTB}",
"name": "Fuel Tank 625 Liter",
"weight": 1150,
}
inject_weapons(WeaponsSEM)
@planemod
class VSN_SEM(PlaneType):
id = "VSN_SEM"
flyable = True
height = 4
width = 13.05
length = 18
fuel_max = 6103
max_speed = 2649.996
chaff = 90
flare = 45
charge_total = 180
chaff_charge_size = 1
flare_charge_size = 2
category = "Interceptor" # {78EFB7A2-FD52-4b57-A6A6-3BF0E1D6555F}
radio_frequency = 127.5
livery_name = "VSN_SEM" # from type
class Pylon1:
Smokewinder___red = (1, Weapons.Smokewinder___red)
Smokewinder___green = (1, Weapons.Smokewinder___green)
Smokewinder___blue = (1, Weapons.Smokewinder___blue)
Smokewinder___white = (1, Weapons.Smokewinder___white)
Smokewinder___yellow = (1, Weapons.Smokewinder___yellow)
AN_ASQ_T50_TCTS_Pod___ACMI_Pod = (1, Weapons.AN_ASQ_T50_TCTS_Pod___ACMI_Pod)
Mk_81___250lb_GP_Bomb_LD = (1, Weapons.Mk_81___250lb_GP_Bomb_LD)
Mk_82___500lb_GP_Bomb_LD = (1, Weapons.Mk_82___500lb_GP_Bomb_LD)
R550_Magic_2_IR_AAM = (1, Weapons.R550_Magic_2_IR_AAM)
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
1,
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
ALQ_131___ECM_Pod = (1, Weapons.ALQ_131___ECM_Pod)
AIM_9M_Sidewinder_IR_AAM = (1, Weapons.AIM_9M_Sidewinder_IR_AAM)
AIM_9P_Sidewinder_IR_AAM = (1, Weapons.AIM_9P_Sidewinder_IR_AAM)
class Pylon2:
AGM_65K___Maverick_K__CCD_Imp_ASM_ = (
2,
Weapons.AGM_65K___Maverick_K__CCD_Imp_ASM_,
)
GBU_12___500lb_Laser_Guided_Bomb = (2, Weapons.GBU_12___500lb_Laser_Guided_Bomb)
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
2,
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
Mk_81___250lb_GP_Bomb_LD = (2, Weapons.Mk_81___250lb_GP_Bomb_LD)
Mk_82___500lb_GP_Bomb_LD = (2, Weapons.Mk_82___500lb_GP_Bomb_LD)
Fuel_Tank_1100_Liter = (2, WeaponsSEM.Fuel_Tank_1100_Liter)
Fuel_Tank_625_Liter = (2, WeaponsSEM.Fuel_Tank_625_Liter)
# ERRR <CLEAN>
class Pylon5:
L_081_Fantasmagoria_ELINT_pod = (5, Weapons.L_081_Fantasmagoria_ELINT_pod)
class Pylon6:
MER2_with_2_x_Mk_82___500lb_GP_Bombs_LD = (
6,
Weapons.MER2_with_2_x_Mk_82___500lb_GP_Bombs_LD,
)
AGM_65K___Maverick_K__CCD_Imp_ASM_ = (
6,
Weapons.AGM_65K___Maverick_K__CCD_Imp_ASM_,
)
# ERRR <CLEAN>
class Pylon10:
AGM_65K___Maverick_K__CCD_Imp_ASM_ = (
10,
Weapons.AGM_65K___Maverick_K__CCD_Imp_ASM_,
)
GBU_12___500lb_Laser_Guided_Bomb = (
10,
Weapons.GBU_12___500lb_Laser_Guided_Bomb,
)
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
10,
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
Mk_81___250lb_GP_Bomb_LD = (10, Weapons.Mk_81___250lb_GP_Bomb_LD)
Mk_82___500lb_GP_Bomb_LD = (10, Weapons.Mk_82___500lb_GP_Bomb_LD)
Fuel_Tank_1100_Liter = (10, WeaponsSEM.Fuel_Tank_1100_Liter)
Fuel_Tank_625_Liter = (10, WeaponsSEM.Fuel_Tank_625_Liter)
class Pylon11:
Smokewinder___red = (11, Weapons.Smokewinder___red)
Smokewinder___green = (11, Weapons.Smokewinder___green)
Smokewinder___blue = (11, Weapons.Smokewinder___blue)
Smokewinder___white = (11, Weapons.Smokewinder___white)
Smokewinder___yellow = (11, Weapons.Smokewinder___yellow)
AN_ASQ_T50_TCTS_Pod___ACMI_Pod = (11, Weapons.AN_ASQ_T50_TCTS_Pod___ACMI_Pod)
Mk_81___250lb_GP_Bomb_LD = (11, Weapons.Mk_81___250lb_GP_Bomb_LD)
Mk_82___500lb_GP_Bomb_LD = (11, Weapons.Mk_82___500lb_GP_Bomb_LD)
R550_Magic_2_IR_AAM = (11, Weapons.R550_Magic_2_IR_AAM)
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
11,
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
ALQ_131___ECM_Pod = (11, Weapons.ALQ_131___ECM_Pod)
AIM_9M_Sidewinder_IR_AAM = (11, Weapons.AIM_9M_Sidewinder_IR_AAM)
AIM_9P_Sidewinder_IR_AAM = (11, Weapons.AIM_9P_Sidewinder_IR_AAM)
pylons = {1, 2, 3, 5, 6, 9, 10, 11}
tasks = [
task.CAP,
task.Escort,
task.FighterSweep,
task.Intercept,
task.Reconnaissance,
task.GroundAttack,
task.CAS,
task.AFAC,
task.RunwayAttack,
]
task_default = task.FighterSweep

View File

@ -137,7 +137,8 @@ def run_ui(game: Optional[Game], ui_flags: UiFlags) -> None:
# Replace DCS Mission scripting file to allow DCS Retribution to work # Replace DCS Mission scripting file to allow DCS Retribution to work
try: try:
liberation_install.replace_mission_scripting_file() liberation_install.replace_mission_scripting_file()
except: except Exception as e:
logging.error(e)
error_dialog = QtWidgets.QErrorMessage() error_dialog = QtWidgets.QErrorMessage()
error_dialog.setWindowTitle("Wrong DCS installation directory.") error_dialog.setWindowTitle("Wrong DCS installation directory.")
error_dialog.showMessage( error_dialog.showMessage(
@ -317,6 +318,7 @@ def create_game(
no_player_navy=False, no_player_navy=False,
no_enemy_navy=False, no_enemy_navy=False,
tgo_config=campaign.load_ground_forces_config(), tgo_config=campaign.load_ground_forces_config(),
carrier_config=campaign.load_carrier_config(),
), ),
ModSettings( ModSettings(
a4_skyhawk=False, a4_skyhawk=False,
@ -325,6 +327,7 @@ def create_game(
fa_18efg=False, fa_18efg=False,
fa18ef_tanker=False, fa18ef_tanker=False,
f4bc_phantom=False, f4bc_phantom=False,
f9f_panther=False,
f22_raptor=False, f22_raptor=False,
f84g_thunderjet=False, f84g_thunderjet=False,
f100_supersabre=False, f100_supersabre=False,

View File

@ -141,6 +141,7 @@ class QFlightList(QListView):
) )
return return
self.package_model.add_flight(clone) self.package_model.add_flight(clone)
EventStream.put_nowait(GameUpdateEvents().new_flight(clone))
def cancel_or_abort_flight(self, index: QModelIndex) -> None: def cancel_or_abort_flight(self, index: QModelIndex) -> None:
self.package_model.cancel_or_abort_flight_at_index(index) self.package_model.cancel_or_abort_flight_at_index(index)
@ -338,6 +339,10 @@ class QPackageList(QListView):
) )
return return
self.ato_model.add_package(clone) self.ato_model.add_package(clone)
events = GameUpdateEvents()
for f in clone.flights:
events.new_flight(f)
EventStream.put_nowait(events)
def delete_package(self, index: QModelIndex) -> None: def delete_package(self, index: QModelIndex) -> None:
self.ato_model.cancel_or_abort_package_at_index(index) self.ato_model.cancel_or_abort_package_at_index(index)

View File

@ -97,6 +97,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
wpt.description = f"Friendly unit: {target.name}" wpt.description = f"Friendly unit: {target.name}"
else: else:
wpt.description = f"Enemy unit: {target.name}" wpt.description = f"Enemy unit: {target.name}"
wpt.waypoint_type = FlightWaypointType.TARGET_POINT
i = add_model_item(i, model, wpt.pretty_name, wpt) i = add_model_item(i, model, wpt.pretty_name, wpt)
if self.include_airbases: if self.include_airbases:

View File

@ -109,7 +109,7 @@ class QLiberationWindow(QMainWindow):
if last_save_file: if last_save_file:
logging.info("Loading last saved game : " + str(last_save_file)) logging.info("Loading last saved game : " + str(last_save_file))
game = persistency.load_game(last_save_file) game = persistency.load_game(last_save_file)
self.migrate_game(game, last_save_file) game = self.migrate_game(game, last_save_file)
self.onGameGenerated(game) self.onGameGenerated(game)
self.updateWindowTitle(last_save_file if game else None) self.updateWindowTitle(last_save_file if game else None)
else: else:
@ -342,7 +342,7 @@ class QLiberationWindow(QMainWindow):
) )
if file is not None and file[0] != "": if file is not None and file[0] != "":
game = persistency.load_game(file[0]) game = persistency.load_game(file[0])
self.migrate_game(game, file[0]) game = self.migrate_game(game, file[0])
GameUpdateSignal.get_instance().game_loaded.emit(game) GameUpdateSignal.get_instance().game_loaded.emit(game)
self.updateWindowTitle(file[0]) self.updateWindowTitle(file[0])
@ -350,8 +350,16 @@ class QLiberationWindow(QMainWindow):
def migrate_game(self, game, path): def migrate_game(self, game, path):
if game: if game:
is_liberation = ".liberation" in path is_liberation = ".liberation" in path
try:
Migrator(game, is_liberation) Migrator(game, is_liberation)
return game
except Exception:
self.incompatible_save_popup(path)
else: else:
self.incompatible_save_popup(path)
return None
def incompatible_save_popup(self, path):
relative_path = Path(path) relative_path = Path(path)
QMessageBox.critical( QMessageBox.critical(
self, self,

View File

@ -239,5 +239,7 @@ class QWaitingForMissionResultWindow(QDialog):
self.sim_controller.set_game(self.game) self.sim_controller.set_game(self.game)
for _, f in self.game.db.flights.objects.items(): for _, f in self.game.db.flights.objects.items():
f.state.reinitialize(self.game.conditions.start_time) f.state.reinitialize(self.game.conditions.start_time)
for cp in self.game.theater.controlpoints:
cp.release_parking_slots()
GameUpdateSignal.get_instance().updateGame(self.game) GameUpdateSignal.get_instance().updateGame(self.game)
self.close() self.close()

View File

@ -32,6 +32,7 @@ from game.theater.theatergroundobject import (
EwrGroundObject, EwrGroundObject,
SamGroundObject, SamGroundObject,
VehicleGroupGroundObject, VehicleGroupGroundObject,
ShipGroundObject,
) )
from qt_ui.uiconstants import EVENT_ICONS from qt_ui.uiconstants import EVENT_ICONS
@ -188,7 +189,8 @@ class QGroundObjectTemplateLayout(QGroupBox):
@property @property
def affordable(self) -> bool: def affordable(self) -> bool:
return self.cost <= self.game.blue.budget coalition = self.ground_object.coalition
return self.cost <= coalition.budget or self.game.turn == 0
def add_theater_group( def add_theater_group(
self, group_name: str, force_group: ForceGroup, groups: list[TgoLayoutUnitGroup] self, group_name: str, force_group: ForceGroup, groups: list[TgoLayoutUnitGroup]
@ -226,7 +228,8 @@ class QGroundObjectTemplateLayout(QGroupBox):
self.game.theater.heading_to_conflict_from(self.ground_object.position) self.game.theater.heading_to_conflict_from(self.ground_object.position)
or self.ground_object.heading or self.ground_object.heading
) )
self.game.blue.budget -= self.cost coalition = self.ground_object.coalition
coalition.budget -= self.cost if self.game.turn else 0
self.ground_object.groups = [] self.ground_object.groups = []
for group_name, groups in self.layout_model.groups.items(): for group_name, groups in self.layout_model.groups.items():
for group in groups: for group in groups:
@ -276,13 +279,17 @@ class QGroundObjectBuyMenu(QDialog):
elif isinstance(ground_object, EwrGroundObject): elif isinstance(ground_object, EwrGroundObject):
role = GroupRole.AIR_DEFENSE role = GroupRole.AIR_DEFENSE
tasks.append(GroupTask.EARLY_WARNING_RADAR) tasks.append(GroupTask.EARLY_WARNING_RADAR)
elif isinstance(ground_object, ShipGroundObject):
role = GroupRole.NAVAL
tasks.append(GroupTask.NAVY)
else: else:
raise NotImplementedError(f"Unhandled TGO type {ground_object.__class__}") raise NotImplementedError(f"Unhandled TGO type {ground_object.__class__}")
if not tasks: if not tasks:
tasks = role.tasks tasks = role.tasks
for group in game.blue.armed_forces.groups_for_tasks(tasks): coalition = ground_object.coalition
for group in coalition.armed_forces.groups_for_tasks(tasks):
self.force_group_selector.addItem(group.name, userData=group) self.force_group_selector.addItem(group.name, userData=group)
self.force_group_selector.setEnabled(self.force_group_selector.count() > 1) self.force_group_selector.setEnabled(self.force_group_selector.count() > 1)
self.force_group_selector.adjustSize() self.force_group_selector.adjustSize()

View File

@ -104,15 +104,23 @@ class QGroundObjectMenu(QDialog):
self.buy_replace.clicked.connect(self.buy_group) self.buy_replace.clicked.connect(self.buy_group)
self.buy_replace.setProperty("style", "btn-success") self.buy_replace.setProperty("style", "btn-success")
if self.ground_object.purchasable: if self.ground_object.purchasable or self.game.turn == 0:
if self.total_value > 0: if self.total_value > 0:
self.actionLayout.addWidget(self.sell_all_button) self.actionLayout.addWidget(self.sell_all_button)
self.actionLayout.addWidget(self.buy_replace) self.actionLayout.addWidget(self.buy_replace)
if self.cp.captured and self.ground_object.purchasable: if self.show_buy_sell_actions and (
self.ground_object.purchasable or self.game.turn == 0
):
self.mainLayout.addLayout(self.actionLayout) self.mainLayout.addLayout(self.actionLayout)
self.setLayout(self.mainLayout) self.setLayout(self.mainLayout)
@property
def show_buy_sell_actions(self) -> bool:
buysell_allowed = self.game.settings.enable_enemy_buy_sell
buysell_allowed |= self.cp.captured
return buysell_allowed
def doLayout(self): def doLayout(self):
self.update_total_value() self.update_total_value()
self.intelBox = QGroupBox("Units :") self.intelBox = QGroupBox("Units :")
@ -240,7 +248,7 @@ class QGroundObjectMenu(QDialog):
self.actionLayout.addWidget(self.sell_all_button) self.actionLayout.addWidget(self.sell_all_button)
self.actionLayout.addWidget(self.buy_replace) self.actionLayout.addWidget(self.buy_replace)
if self.cp.captured and self.ground_object.purchasable: if self.show_buy_sell_actions and self.ground_object.purchasable:
self.mainLayout.addLayout(self.actionLayout) self.mainLayout.addLayout(self.actionLayout)
except Exception as e: except Exception as e:
logging.exception(e) logging.exception(e)
@ -276,7 +284,8 @@ class QGroundObjectMenu(QDialog):
def sell_all(self): def sell_all(self):
self.update_total_value() self.update_total_value()
self.game.blue.budget += self.total_value coalition = self.ground_object.coalition
coalition.budget += self.total_value
self.ground_object.groups = [] self.ground_object.groups = []
self.update_game() self.update_game()
@ -296,7 +305,10 @@ class QGroundObjectMenu(QDialog):
for package in self.game.ato_for(player=False).packages for package in self.game.ato_for(player=False).packages
): ):
# Replan if the tgo was a target of the redfor # Replan if the tgo was a target of the redfor
self.game.initialize_turn(events, for_red=True, for_blue=False) coalition = self.ground_object.coalition
self.game.initialize_turn(
events, for_red=coalition.player, for_blue=not coalition.player
)
EventStream.put_nowait(events) EventStream.put_nowait(events)
GameUpdateSignal.get_instance().updateGame(self.game) GameUpdateSignal.get_instance().updateGame(self.game)
# Refresh the dialog # Refresh the dialog

View File

@ -223,7 +223,7 @@ class QAutoCreateDialog(QDialog):
self.game.settings, self.game.settings,
) )
now = self.package_model.game_model.sim_controller.current_time_in_sim now = self.package_model.game_model.sim_controller.current_time_in_sim
package = pff.plan_mission(pm, 1, now, tracer) package = pff.plan_mission(pm, 1, now, tracer, ignore_range=True)
if package: if package:
package.set_tot_asap(now) package.set_tot_asap(now)
self.package = package self.package = package

View File

@ -17,6 +17,7 @@ from game.ato.flight import Flight
from game.ato.flightmember import FlightMember from game.ato.flightmember import FlightMember
from game.ato.loadouts import Loadout from game.ato.loadouts import Loadout
from qt_ui.widgets.QLabeledWidget import QLabeledWidget from qt_ui.widgets.QLabeledWidget import QLabeledWidget
from qt_ui.widgets.combos.QSquadronLiverySelector import SquadronLiverySelector
from .QLoadoutEditor import QLoadoutEditor from .QLoadoutEditor import QLoadoutEditor
from .ownlasercodeinfo import OwnLaserCodeInfo from .ownlasercodeinfo import OwnLaserCodeInfo
from .propertyeditor import PropertyEditor from .propertyeditor import PropertyEditor
@ -141,6 +142,24 @@ class QFlightPayloadTab(QFrame):
) )
) )
hbox = QHBoxLayout()
self.same_livery_for_all_checkbox = QCheckBox(
"Use same livery for all flight members"
)
self.same_livery_for_all_checkbox.setChecked(
self.flight.use_same_livery_for_all_members
)
self.same_livery_for_all_checkbox.toggled.connect(self.on_same_livery_toggled)
hbox.addWidget(self.same_livery_for_all_checkbox)
self.livery_selector = SquadronLiverySelector(self.flight.squadron)
self.livery_selector.insertItem(0, "Default", None)
self.livery_selector.setCurrentIndex(
self.livery_selector.findData(self.member_selector.selected_member.livery)
)
self.livery_selector.currentIndexChanged.connect(self.on_livery_change)
hbox.addWidget(self.livery_selector)
layout.addLayout(hbox)
scroll_content = QWidget() scroll_content = QWidget()
scrolling_layout = QVBoxLayout() scrolling_layout = QVBoxLayout()
scroll_content.setLayout(scrolling_layout) scroll_content.setLayout(scrolling_layout)
@ -212,6 +231,9 @@ class QFlightPayloadTab(QFrame):
self.property_editor.set_flight_member(member) self.property_editor.set_flight_member(member)
self.loadout_selector.setCurrentText(member.loadout.name) self.loadout_selector.setCurrentText(member.loadout.name)
self.loadout_selector.setDisabled(member.loadout.is_custom) self.loadout_selector.setDisabled(member.loadout.is_custom)
self.livery_selector.setCurrentIndex(
self.livery_selector.findData(member.livery)
)
self.payload_editor.set_flight_member(member) self.payload_editor.set_flight_member(member)
self.weapon_laser_code_selector.set_flight_member(member) self.weapon_laser_code_selector.set_flight_member(member)
self.own_laser_code_info.set_flight_member(member) self.own_laser_code_info.set_flight_member(member)
@ -222,9 +244,13 @@ class QFlightPayloadTab(QFrame):
self.payload_editor.setDisabled( self.payload_editor.setDisabled(
self.flight.use_same_loadout_for_all_members self.flight.use_same_loadout_for_all_members
) )
self.livery_selector.setDisabled(
self.flight.use_same_livery_for_all_members
)
else: else:
self.loadout_selector.setEnabled(True) self.loadout_selector.setEnabled(True)
self.payload_editor.setEnabled(True) self.payload_editor.setEnabled(True)
self.livery_selector.setEnabled(True)
def loadout_at(self, index: int) -> Loadout: def loadout_at(self, index: int) -> Loadout:
loadout = self.loadout_selector.itemData(index) loadout = self.loadout_selector.itemData(index)
@ -273,3 +299,20 @@ class QFlightPayloadTab(QFrame):
self.rebind_to_selected_member() self.rebind_to_selected_member()
else: else:
self.flight.roster.use_distinct_loadouts_for_each_member() self.flight.roster.use_distinct_loadouts_for_each_member()
def on_same_livery_toggled(self, checked: bool) -> None:
self.flight.use_same_livery_for_all_members = checked
if self.member_selector.value():
self.livery_selector.setDisabled(checked)
if checked:
self.flight.roster.use_same_livery_for_all_members()
if self.member_selector.value():
self.rebind_to_selected_member()
def on_livery_change(self) -> None:
livery = self.livery_selector.currentData()
if self.flight.use_same_livery_for_all_members:
for m in self.flight.roster.members:
m.livery = livery
else:
self.member_selector.selected_member.livery = livery

View File

@ -86,7 +86,7 @@ class QFlightWaypointList(QTableView):
self.model.setItem(row, 0, QWaypointItem(waypoint, row)) self.model.setItem(row, 0, QWaypointItem(waypoint, row))
altitude = int(waypoint.alt.feet) altitude = round(waypoint.alt.feet)
altitude_item = QStandardItem(f"{altitude}") altitude_item = QStandardItem(f"{altitude}")
altitude_item.setEditable(True) altitude_item.setEditable(True)
self.model.setItem(row, 1, altitude_item) self.model.setItem(row, 1, altitude_item)

View File

@ -162,9 +162,9 @@ class QFlightWaypointTab(QFrame):
assert isinstance(self.flight.flight_plan, CustomFlightPlan) assert isinstance(self.flight.flight_plan, CustomFlightPlan)
self.flight.flight_plan.layout.custom_waypoints.remove(waypoint) self.flight.flight_plan.layout.custom_waypoints.remove(waypoint)
def confirm_degrade(self) -> bool: def confirm_degrade(self, parent: Optional[QWidget] = None) -> bool:
result = QMessageBox.warning( result = QMessageBox.warning(
self, parent if parent else self,
"Degrade flight-plan?", "Degrade flight-plan?",
"Deleting the selected waypoint(s) will require degradation to a custom flight-plan. " "Deleting the selected waypoint(s) will require degradation to a custom flight-plan. "
"A custom flight-plan will no longer respect the TOTs of the package.<br><br>" "A custom flight-plan will no longer respect the TOTs of the package.<br><br>"
@ -184,12 +184,6 @@ class QFlightWaypointTab(QFrame):
def on_waypoints_added(self, waypoints: Iterable[FlightWaypoint]) -> None: def on_waypoints_added(self, waypoints: Iterable[FlightWaypoint]) -> None:
if not waypoints: if not waypoints:
return return
if not self.flight.flight_plan.is_custom:
confirmed = self.confirm_degrade()
if not confirmed:
return
self.degrade_to_custom_flight_plan()
assert isinstance(self.flight.flight_plan, CustomFlightPlan)
self.flight.flight_plan.layout.custom_waypoints.extend(waypoints) self.flight.flight_plan.layout.custom_waypoints.extend(waypoints)
self.add_rows(len(list(waypoints))) self.add_rows(len(list(waypoints)))

View File

@ -84,9 +84,11 @@ class NewGameWizard(QtWidgets.QWizard):
no_player_navy=self.field("no_player_navy"), no_player_navy=self.field("no_player_navy"),
no_enemy_navy=self.field("no_enemy_navy"), no_enemy_navy=self.field("no_enemy_navy"),
tgo_config=campaign.load_ground_forces_config(), tgo_config=campaign.load_ground_forces_config(),
carrier_config=campaign.load_carrier_config(),
squadrons_start_full=self.field("squadrons_start_full"), squadrons_start_full=self.field("squadrons_start_full"),
) )
mod_settings = ModSettings( mod_settings = ModSettings(
f9f_panther=self.field("f9f_panther"),
a4_skyhawk=self.field("a4_skyhawk"), a4_skyhawk=self.field("a4_skyhawk"),
a6a_intruder=self.field("a6a_intruder"), a6a_intruder=self.field("a6a_intruder"),
a7e_corsair2=self.field("a7e_corsair2"), a7e_corsair2=self.field("a7e_corsair2"),
@ -105,6 +107,7 @@ class NewGameWizard(QtWidgets.QWizard):
irondome=self.field("irondome"), irondome=self.field("irondome"),
uh_60l=self.field("uh_60l"), uh_60l=self.field("uh_60l"),
jas39_gripen=self.field("jas39_gripen"), jas39_gripen=self.field("jas39_gripen"),
super_etendard=self.field("super_etendard"),
su30_flanker_h=self.field("su30_flanker_h"), su30_flanker_h=self.field("su30_flanker_h"),
su57_felon=self.field("su57_felon"), su57_felon=self.field("su57_felon"),
ov10a_bronco=self.field("ov10a_bronco"), ov10a_bronco=self.field("ov10a_bronco"),

View File

@ -100,6 +100,8 @@ class GeneratorOptions(QtWidgets.QWizardPage):
self.registerField("uh_60l", self.uh_60l) self.registerField("uh_60l", self.uh_60l)
self.f4bc_phantom = QtWidgets.QCheckBox() self.f4bc_phantom = QtWidgets.QCheckBox()
self.registerField("f4bc_phantom", self.f4bc_phantom) self.registerField("f4bc_phantom", self.f4bc_phantom)
self.f9f_panther = QtWidgets.QCheckBox()
self.registerField("f9f_panther", self.f9f_panther)
self.f15d_baz = QtWidgets.QCheckBox() self.f15d_baz = QtWidgets.QCheckBox()
self.registerField("f15d_baz", self.f15d_baz) self.registerField("f15d_baz", self.f15d_baz)
self.f_15_idf = QtWidgets.QCheckBox() self.f_15_idf = QtWidgets.QCheckBox()
@ -122,6 +124,8 @@ class GeneratorOptions(QtWidgets.QWizardPage):
self.registerField("f105_thunderchief", self.f105_thunderchief) self.registerField("f105_thunderchief", self.f105_thunderchief)
self.jas39_gripen = QtWidgets.QCheckBox() self.jas39_gripen = QtWidgets.QCheckBox()
self.registerField("jas39_gripen", self.jas39_gripen) self.registerField("jas39_gripen", self.jas39_gripen)
self.super_etendard = QtWidgets.QCheckBox()
self.registerField("super_etendard", self.super_etendard)
self.su30_flanker_h = QtWidgets.QCheckBox() self.su30_flanker_h = QtWidgets.QCheckBox()
self.registerField("su30_flanker_h", self.su30_flanker_h) self.registerField("su30_flanker_h", self.su30_flanker_h)
self.su57_felon = QtWidgets.QCheckBox() self.su57_felon = QtWidgets.QCheckBox()
@ -150,6 +154,7 @@ class GeneratorOptions(QtWidgets.QWizardPage):
modLayout_row = 1 modLayout_row = 1
mod_pairs = [ mod_pairs = [
("F9F Panther (v2.8.7.101)", self.f9f_panther),
("A-4E Skyhawk (v2.2.0)", self.a4_skyhawk), ("A-4E Skyhawk (v2.2.0)", self.a4_skyhawk),
("A-6A Intruder (v2.7.5.01)", self.a6a_intruder), ("A-6A Intruder (v2.7.5.01)", self.a6a_intruder),
("A-7E Corsair II", self.a7e_corsair2), ("A-7E Corsair II", self.a7e_corsair2),
@ -158,7 +163,7 @@ class GeneratorOptions(QtWidgets.QWizardPage):
("F-15D Baz (v1.0)", self.f15d_baz), ("F-15D Baz (v1.0)", self.f15d_baz),
("F-15I Ra'am (v1.0 by IDF Mods Project)", self.f_15_idf), ("F-15I Ra'am (v1.0 by IDF Mods Project)", self.f_15_idf),
("F-16I Sufa & F-16D (v3.6 by IDF Mods Project)", self.f_16_idf), ("F-16I Sufa & F-16D (v3.6 by IDF Mods Project)", self.f_16_idf),
("F/A-18E/F/G Super Hornet (version 2.1)", self.fa_18efg), ("F/A-18E/F/G Super Hornet (version 2.2.5)", self.fa_18efg),
("F/A-18E/F Super Hornet AI Tanker (version 1.4)", self.fa18ef_tanker), ("F/A-18E/F Super Hornet AI Tanker (version 1.4)", self.fa18ef_tanker),
("F-22A Raptor", self.f22_raptor), ("F-22A Raptor", self.f22_raptor),
("F-84G Thunderjet (v2.5.7.01)", self.f84g_thunderjet), ("F-84G Thunderjet (v2.5.7.01)", self.f84g_thunderjet),
@ -170,6 +175,7 @@ class GeneratorOptions(QtWidgets.QWizardPage):
("Swedish Military Assets pack (1.10)", self.swedishmilitaryassetspack), ("Swedish Military Assets pack (1.10)", self.swedishmilitaryassetspack),
("JAS 39 Gripen (v1.8.5-beta)", self.jas39_gripen), ("JAS 39 Gripen (v1.8.5-beta)", self.jas39_gripen),
("OV-10A Bronco", self.ov10a_bronco), ("OV-10A Bronco", self.ov10a_bronco),
("Super Étendard (v2.5.5)", self.super_etendard),
("Su-30 Flanker-H (V2.7.3 beta)", self.su30_flanker_h), ("Su-30 Flanker-H (V2.7.3 beta)", self.su30_flanker_h),
("Su-57 Felon (build-04)", self.su57_felon), ("Su-57 Felon (build-04)", self.su57_felon),
("UH-60L Black Hawk (v1.3.1)", self.uh_60l), ("UH-60L Black Hawk (v1.3.1)", self.uh_60l),
@ -212,6 +218,7 @@ class GeneratorOptions(QtWidgets.QWizardPage):
self.no_enemy_navy.setChecked(s.get("no_enemy_navy", False)) self.no_enemy_navy.setChecked(s.get("no_enemy_navy", False))
self.squadrons_start_full.setChecked(s.get("squadron_start_full", False)) self.squadrons_start_full.setChecked(s.get("squadron_start_full", False))
self.f9f_panther.setChecked(s.get("f9f_panther", False))
self.a4_skyhawk.setChecked(s.get("a4_skyhawk", False)) self.a4_skyhawk.setChecked(s.get("a4_skyhawk", False))
self.a6a_intruder.setChecked(s.get("a6a_intruder", False)) self.a6a_intruder.setChecked(s.get("a6a_intruder", False))
self.a7e_corsair2.setChecked(s.get("a7e_corsair2", False)) self.a7e_corsair2.setChecked(s.get("a7e_corsair2", False))
@ -228,6 +235,7 @@ class GeneratorOptions(QtWidgets.QWizardPage):
self.f104_starfighter.setChecked(s.get("f104_starfighter", False)) self.f104_starfighter.setChecked(s.get("f104_starfighter", False))
self.f105_thunderchief.setChecked(s.get("f105_thunderchief", False)) self.f105_thunderchief.setChecked(s.get("f105_thunderchief", False))
self.jas39_gripen.setChecked(s.get("jas39_gripen", False)) self.jas39_gripen.setChecked(s.get("jas39_gripen", False))
self.super_etendard.setChecked(s.get("super_etendard", False))
self.su30_flanker_h.setChecked(s.get("su30_flanker_h", False)) self.su30_flanker_h.setChecked(s.get("su30_flanker_h", False))
self.su57_felon.setChecked(s.get("su57_felon", False)) self.su57_felon.setChecked(s.get("su57_felon", False))
self.ov10a_bronco.setChecked(s.get("ov10a_bronco", False)) self.ov10a_bronco.setChecked(s.get("ov10a_bronco", False))

View File

@ -113,6 +113,15 @@ class CheatSettingsBox(QGroupBox):
) )
self.main_layout.addLayout(self.air_wing_cheat) self.main_layout.addLayout(self.air_wing_cheat)
# Buy/Sell actions for OPFOR
self.opfor_buysell_checkbox = QCheckBox()
self.opfor_buysell_checkbox.setChecked(sc.settings.enable_enemy_buy_sell)
self.opfor_buysell_checkbox.toggled.connect(apply_settings)
self.redfor_buysell_cheat = QLabeledWidget(
"Enable OPFOR Buy/Sell actions Cheat:", self.opfor_buysell_checkbox
)
self.main_layout.addLayout(self.redfor_buysell_cheat)
@property @property
def show_red_ato(self) -> bool: def show_red_ato(self) -> bool:
return self.red_ato_checkbox.isChecked() return self.red_ato_checkbox.isChecked()
@ -137,6 +146,10 @@ class CheatSettingsBox(QGroupBox):
def enable_air_wing_cheats(self) -> bool: def enable_air_wing_cheats(self) -> bool:
return self.air_wing_adjustments_checkbox.isChecked() return self.air_wing_adjustments_checkbox.isChecked()
@property
def enable_redfor_buysell(self) -> bool:
return self.opfor_buysell_checkbox.isChecked()
class AutoSettingsLayout(QGridLayout): class AutoSettingsLayout(QGridLayout):
def __init__( def __init__(
@ -495,6 +508,7 @@ class QSettingsWidget(QtWidgets.QWizardPage, SettingsContainer):
self.settings.enable_air_wing_adjustments = ( self.settings.enable_air_wing_adjustments = (
self.cheat_options.enable_air_wing_cheats self.cheat_options.enable_air_wing_cheats
) )
self.settings.enable_enemy_buy_sell = self.cheat_options.enable_redfor_buysell
if self.game: if self.game:
events = GameUpdateEvents() events = GameUpdateEvents()
@ -521,6 +535,15 @@ class QSettingsWidget(QtWidgets.QWizardPage, SettingsContainer):
self.cheat_options.transfer_cheat_checkbox.setChecked( self.cheat_options.transfer_cheat_checkbox.setChecked(
self.settings.enable_transfer_cheat self.settings.enable_transfer_cheat
) )
self.cheat_options.base_runway_state_cheat_checkbox.setChecked(
self.settings.enable_runway_state_cheat
)
self.cheat_options.air_wing_adjustments_checkbox.setChecked(
self.settings.enable_air_wing_adjustments
)
self.cheat_options.opfor_buysell_checkbox.setChecked(
self.settings.enable_enemy_buy_sell
)
self.pluginsPage.update_from_settings() self.pluginsPage.update_from_settings()
self.pluginsOptionsPage.update_from_settings() self.pluginsOptionsPage.update_from_settings()

View File

@ -1,6 +1,6 @@
altgraph==0.17.4 altgraph==0.17.4
anyio==3.7.1 anyio==3.7.1
asgiref==3.7.2 asgiref==3.8.1
atomicwrites==1.4.1 atomicwrites==1.4.1
attrs==23.2.0 attrs==23.2.0
black==23.9.1 black==23.9.1
@ -9,61 +9,61 @@ cfgv==3.4.0
click==8.1.7 click==8.1.7
colorama==0.4.6 colorama==0.4.6
distlib==0.3.8 distlib==0.3.8
Faker==22.6.0 Faker==24.14.0
fastapi==0.109.2 fastapi==0.110.2
filelock==3.13.1 filelock==3.13.4
h11==0.14.0 h11==0.14.0
httptools==0.6.1 httptools==0.6.1
identify==2.5.33 identify==2.5.36
idna==3.6 idna==3.7
iniconfig==2.0.0 iniconfig==2.0.0
Jinja2==3.1.3 Jinja2==3.1.3
lupa==2.0 lupa==2.1
MarkupSafe==2.1.5 MarkupSafe==2.1.5
mypy==1.8.0 mypy==1.10.0
mypy-extensions==1.0.0 mypy-extensions==1.0.0
nodeenv==1.8.0 nodeenv==1.8.0
packaging==23.2 packaging==24.0
pathspec==0.12.1 pathspec==0.12.1
pefile==2023.2.7 pefile==2023.2.7
Pillow==10.2.0 Pillow==10.3.0
platformdirs==4.2.0 platformdirs==4.2.1
pluggy==1.4.0 pluggy==1.5.0
pre-commit==3.6.0 pre-commit==3.7.0
pydantic==2.6.0 pydantic==2.7.1
pydantic-settings==2.1.0 pydantic-settings==2.2.1
-e git+https://github.com/dcs-retribution/pydcs@353f5b177dd406122a83e8572fd6ca54adf84389#egg=pydcs pydcs @ git+https://github.com/dcs-retribution/pydcs@ab2f274c0882f13918ca6f4be3c6c8bdc71818e0
pyinstaller==5.13.2 pyinstaller==5.13.2
pyinstaller-hooks-contrib==2024.0 pyinstaller-hooks-contrib==2024.0
pyparsing==3.1.1 pyparsing==3.1.2
pyproj==3.6.1 pyproj==3.6.1
pyshp==2.3.1 pyshp==2.3.1
PySide6==6.4.3 PySide6==6.4.2
PySide6-Addons==6.4.3 PySide6-Addons==6.4.2
PySide6-Essentials==6.4.3 PySide6-Essentials==6.4.2
pytest==8.0.0 pytest==8.1.2
pytest-cov==4.1.0 pytest-cov==5.0.0
python-dateutil==2.8.2 python-dateutil==2.9.0.post0
python-dotenv==1.0.1 python-dotenv==1.0.1
pywin32-ctypes==0.2.2 pywin32-ctypes==0.2.2
PyYAML==6.0.1 PyYAML==6.0.1
Shapely==2.0.2 Shapely==2.0.4
shiboken6==6.4.3 shiboken6==6.4.2
six==1.16.0 six==1.16.0
sniffio==1.3.0 sniffio==1.3.1
starlette==0.36.3 starlette==0.37.2
suntime==1.2.5 suntime==1.3.2
tabulate==0.9.0 tabulate==0.9.0
text-unidecode==1.3 text-unidecode==1.3
toml==0.10.2 toml==0.10.2
tomli==2.0.1 tomli==2.0.1
types-Jinja2==2.11.9 types-Jinja2==2.11.9
types-MarkupSafe==1.1.10 types-MarkupSafe==1.1.10
types-Pillow==10.2.0.20240125 types-Pillow==10.2.0.20240423
types-PyYAML==6.0.12.12 types-PyYAML==6.0.12.20240311
types-tabulate==0.9.0.20240106 types-tabulate==0.9.0.20240106
typing_extensions==4.9.0 typing_extensions==4.11.0
uvicorn==0.27.0.post1 uvicorn==0.29.0
virtualenv==20.25.0 virtualenv==20.26.0
watchgod==0.8.2 watchgod==0.8.2
websockets==12.0 websockets==12.0

View File

@ -38,12 +38,12 @@ squadrons:
#Port Stanley #Port Stanley
1: 1:
- primary: DEAD - primary: DEAD
secondary: air-to-ground secondary: any
aircraft: aircraft:
- AH-64D Apache Longbow - AH-64D Apache Longbow
size: 6 size: 6
- primary: BAI - primary: BAI
secondary: air-to-ground secondary: any
aircraft: aircraft:
- AH-64D Apache Longbow - AH-64D Apache Longbow
size: 6 size: 6
@ -56,14 +56,15 @@ squadrons:
#San Carlos FOB #San Carlos FOB
3: 3:
- primary: BAI - primary: BAI
secondary: air-to-ground secondary: any
aircraft: aircraft:
- Mi-24P Hind-F - Mi-24P Hind-F
size: 6 size: 6
#Goose Green #Goose Green
24: 24:
- primary: DEAD - primary: DEAD
secondary: air-to-ground secondary: any
aircraft: aircraft:
- Ka-50 Hokum III
- Ka-50 Hokum (Blackshark 3) - Ka-50 Hokum (Blackshark 3)
size: 6 size: 6

View File

@ -17,7 +17,7 @@ description:
adversarial nation, dubbed Orangeland. This scenario is designed to adversarial nation, dubbed Orangeland. This scenario is designed to
simulate a mock invasion against Cairo, and presents a valuable simulate a mock invasion against Cairo, and presents a valuable
opportunity for participating nations to refine their joint operational opportunity for participating nations to refine their joint operational
capabilities and improve logistical and tactical interoperability</p><p> capabilities and improve logistical and tactical interoperability.</p><p>
A historic addition to Exercise Bright Star 2025 is the participation of A historic addition to Exercise Bright Star 2025 is the participation of
Israel as an observer nation. This marks a significant milestone, given Israel as an observer nation. This marks a significant milestone, given
the complex historical relations in the region, and symbolises a step the complex historical relations in the region, and symbolises a step
@ -25,11 +25,17 @@ description:
hosting the aggressor faction of the exercise coalition at its airfields, hosting the aggressor faction of the exercise coalition at its airfields,
not only demonstrates the broadening scope of the exercise but also highlights not only demonstrates the broadening scope of the exercise but also highlights
the value of fostering an environment of mutual cooperation and shared the value of fostering an environment of mutual cooperation and shared
security objectives.</p> security objectives.</p><p>
<strong>Note:</strong> There is no overland supply route between Melez and
Wadi al Jandali due to the simulated destruction of the Al Salam Bridge spanning
the Suez Canal. Consequently, ground units will have to be ferried across by
air during the exercise.
miz: exercise_bright_star.miz miz: exercise_bright_star.miz
performance: 1 performance: 1
recommended_start_date: 2025-09-01 recommended_start_date: 2025-09-01
version: "10.7" version: "10.7"
settings:
hercules: true
squadrons: squadrons:
Blue CV-1: Blue CV-1:
- primary: SEAD - primary: SEAD
@ -45,7 +51,7 @@ squadrons:
aircraft: aircraft:
- S-3B Tanker - S-3B Tanker
size: 4 size: 4
Bombers from RAF Fairford: Bombers from Al Udeid Air Base:
- primary: Anti-ship - primary: Anti-ship
secondary: air-to-ground secondary: air-to-ground
aircraft: aircraft:
@ -56,8 +62,13 @@ squadrons:
aircraft: aircraft:
- B-1B Lancer - B-1B Lancer
size: 8 size: 8
# Hatzerim (141) # Ovda
7: 10:
- primary: CAS
secondary: air-to-ground
aircraft:
- A-10C Thunderbolt II (Suite 7)
size: 8
- primary: Escort - primary: Escort
secondary: any secondary: any
aircraft: aircraft:
@ -68,36 +79,47 @@ squadrons:
aircraft: aircraft:
- F-15E Strike Eagle (Suite 4+) - F-15E Strike Eagle (Suite 4+)
size: 16 size: 16
- primary: BAI
secondary: any
aircraft:
- JF-17 Thunder
size: 12
# Ramon Airbase
9:
- primary: DEAD - primary: DEAD
secondary: any secondary: any
aircraft: aircraft:
- F-16CM Fighting Falcon (Block 50) - F-16CM Fighting Falcon (Block 50)
size: 20 size: 20
- primary: BAI
secondary: any
aircraft:
- JF-17 Thunder
size: 16
- primary: BARCAP - primary: BARCAP
secondary: any secondary: any
aircraft: aircraft:
- Mirage 2000C - Mirage 2000C
size: 12 size: 12
# Kedem
12:
- primary: Transport - primary: Transport
secondary: any secondary: any
aircraft: aircraft:
- CH-47D - CH-47D
size: 20 size: 12
- primary: BAI
secondary: any
aircraft:
- AH-64D Apache Longbow
size: 8
- primary: Air Assault - primary: Air Assault
secondary: any secondary: any
aircraft: aircraft:
- UH-60L - UH-60L
- UH-60A - UH-60A
size: 4 size: 4
# Nevatim (106) # Ben-Gurion
8: 24:
- primary: Transport
secondary: any
aircraft:
- C-130J-30 Super Hercules
- C-130
size: 8
- primary: AEW&C - primary: AEW&C
aircraft: aircraft:
- E-3A - E-3A
@ -106,16 +128,12 @@ squadrons:
aircraft: aircraft:
- KC-135 Stratotanker - KC-135 Stratotanker
size: 2 size: 2
- primary: CAS
secondary: air-to-ground
aircraft:
- A-10C Thunderbolt II (Suite 7)
size: 8
# Melez (30) # Melez (30)
5: 5:
- primary: CAS - primary: CAS
secondary: air-to-ground secondary: any
aircraft: aircraft:
- Ka-50 Hokum III
- Ka-50 Hokum (Blackshark 3) - Ka-50 Hokum (Blackshark 3)
size: 4 size: 4
- primary: BAI - primary: BAI
@ -140,7 +158,17 @@ squadrons:
- F-4E Phantom II - F-4E Phantom II
size: 20 size: 20
- primary: DEAD - primary: DEAD
secondary: any secondary:
- BAI
- BARCAP
- CAS
- Escort
- Fighter sweep
- Intercept
- OCA/Aircraft
- OCA/Runway
- Strike
- TARCAP
aircraft: aircraft:
- F-16CM Fighting Falcon (Block 50) - F-16CM Fighting Falcon (Block 50)
size: 20 size: 20
@ -157,7 +185,9 @@ squadrons:
# Cairo West (95) # Cairo West (95)
18: 18:
- primary: Transport - primary: Transport
secondary: any
aircraft: aircraft:
- C-130J-30 Super Hercules
- C-130 - C-130
size: 8 size: 8
- primary: BARCAP - primary: BARCAP

View File

@ -53,7 +53,7 @@ squadrons:
- A-10C Thunderbolt II (Suite 7) - A-10C Thunderbolt II (Suite 7)
size: 8 size: 8
- primary: CAS - primary: CAS
secondary: air-to-ground secondary: any
aircraft: aircraft:
- AH-64D Apache Longbow - AH-64D Apache Longbow
size: 10 size: 10
@ -88,12 +88,13 @@ squadrons:
# Creech # Creech
1: 1:
- primary: CAS - primary: CAS
secondary: air-to-ground secondary: any
aircraft: aircraft:
- Ka-50 Hokum III
- Ka-50 Hokum (Blackshark 3) - Ka-50 Hokum (Blackshark 3)
size: 8 size: 8
- primary: Air Assault - primary: Air Assault
secondary: air-to-ground secondary: any
aircraft: aircraft:
- Mi-24P Hind-F - Mi-24P Hind-F
size: 4 size: 4

View File

@ -17,6 +17,8 @@ miz: grabthars_hammer.miz
performance: 2 performance: 2
recommended_start_date: 1999-12-25 recommended_start_date: 1999-12-25
version: "10.7" version: "10.7"
settings:
hercules: true
squadrons: squadrons:
#Mount Pleasant #Mount Pleasant
2: 2:
@ -35,10 +37,21 @@ squadrons:
aircraft: aircraft:
- F-15C Eagle - F-15C Eagle
size: 8 size: 8
- primary: CAS
secondary: any
aircraft:
- AH-64D Apache Longbow
size: 8
- primary: Refueling - primary: Refueling
aircraft: aircraft:
- KC-135 Stratotanker - KC-135 Stratotanker
size: 2 size: 1
- primary: Air Assault
secondary: any
aircraft:
- C-130J-30 Super Hercules
- C-130
size: 1
#San Julian #San Julian
11: 11:
- primary: BAI - primary: BAI
@ -142,14 +155,15 @@ squadrons:
#Ushuaia #Ushuaia
7: 7:
- primary: CAS - primary: CAS
secondary: air-to-ground secondary: any
aircraft: aircraft:
- Ka-50 Hokum III
- Ka-50 Hokum (Blackshark 3) - Ka-50 Hokum (Blackshark 3)
size: 8 size: 8
#Ushuaia Helo Port #Ushuaia Helo Port
8: 8:
- primary: Air Assault - primary: Air Assault
secondary: air-to-ground secondary: any
aircraft: aircraft:
- Mi-24P Hind-F - Mi-24P Hind-F
size: 8 size: 8

View File

@ -48,7 +48,7 @@ squadrons:
#Tarawa Class LHA #Tarawa Class LHA
Blue-LHA: Blue-LHA:
- primary: DEAD - primary: DEAD
secondary: air-to-ground secondary: any
aircraft: aircraft:
- AH-64D Apache Longbow - AH-64D Apache Longbow
size: 12 size: 12
@ -72,7 +72,7 @@ squadrons:
#Akrotiri #Akrotiri
44: 44:
- primary: BAI - primary: BAI
secondary: air-to-ground secondary: any
aircraft: aircraft:
- AH-1W SuperCobra - AH-1W SuperCobra
size: 4 size: 4
@ -81,8 +81,8 @@ squadrons:
aircraft: aircraft:
- OH-58D Kiowa Warrior - OH-58D Kiowa Warrior
size: 4 size: 4
- primary: Air Assault - primary: Transport
secondary: air-to-ground secondary: any
aircraft: aircraft:
- UH-60A - UH-60A
size: 4 size: 4
@ -95,6 +95,7 @@ squadrons:
#Ercan #Ercan
49: 49:
- primary: Transport - primary: Transport
secondary: any
aircraft: aircraft:
- CH-47D - CH-47D
size: 6 size: 6

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