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"
and provide a link to the applicable build in the field below.
options:
- v1.2.1
- v1.3.1
- Test build
- Development build
- type: input
attributes:
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"
and provide a link to the applicable build in the field below.
options:
- 1.0.0
- 1.3.1
- Test build
- Development build
- type: input
attributes:
label: Test/Development build

View File

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

View File

@ -49,7 +49,7 @@ jobs:
include_image: true
avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
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
uses: stegzilla/discord-notify@v2
@ -60,4 +60,4 @@ jobs:
include_image: true
avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
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)
[![Patreon](https://img.shields.io/badge/patreon-become%20a%20patron-orange?logo=patreon)](https://patreon.com/khopa)
[![Patreon](https://img.shields.io/badge/patreon-become%20a%20patron-orange?logo=patreon)](https://patreon.com/dcsliberation)
[![Download](https://img.shields.io/github/downloads/dcs-retribution/dcs-retribution/total?label=Download)](https://github.com/dcs-retribution/dcs-retribution/releases)

View File

@ -1,3 +1,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
#### 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 Community A-4E-C mod version support to 2.2.0 release.
* **[Modding]** Added F/A-18E/F Super Hornet AI Tanker mod support (Chiller Juice Studios SuperBug Tanker AI version 1.4)
* **[Modding]** Added VSN Super Étendard mod support (v2.5.5)
* **[Modding]** Added F9F Panther mod support (version v2.8.7.101)
* **[Modding]** Updated Irondome support to IDF Assets Pack V1.1, adding support for the David's Sling
* **[Radios]** Added HF-FM band for AN/ARC-222
* **[Radios]** Ability to define preset channels for radios on squadron level (for human pilots only)
@ -41,6 +70,12 @@
* **[Config]** Preference setting to use custom Liberation payloads instead of prioritizing Retribution's default
* **[Config]** Preference setting to configure the server-port on which Retribution's back-end will run
* **[Options]** Made AI jettisoning empty fuel tanks optional (disabled by default)
* **[Options]** Add option (so it can be disabled when fixed in DCS) to force air-starts (except for the slots that work) at Nevatim due to https://forum.dcs.world/topic/335545-29-nevatim-ramp-starts-still-bugged/
* **[Cheat]** Add cheat option to manually manage REDFOR's TGOs
* **[UX]** Buy/Replace TGOs for free before the campaign has started
* **[Data]** Ability to define "cruise" & "combat" altitudes for airplanes
* **[Options]** Option to randomize altitudes for flights with airplanes
* **[Options]** Options to configure/override maximum mission distance for airplanes & helicopters
## Fixes
* **[Mission Generation]** Anti-ship strikes should use "group attack" in their attack-task
@ -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 UI bug where altering an "ahead of package" TOT offset would change the offset back to a "behind pacakge" offset.
* **[UI]** Fixed bug where changing TOT offsets could result in flight startup times that are in the past.
* **[UI]** Flight plan paths are now drawn behind all other map elements, fixing rare cases where they could prevent other UI elements from being clickable.
# 8.1.0

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
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.lasercodes import LaserCode
@ -17,6 +17,7 @@ class FlightMember:
self.tgp_laser_code: LaserCode | None = None
self.weapon_laser_code: LaserCode | None = None
self.properties: dict[str, bool | float | int] = {}
self.livery: Optional[str] = None
def __setstate__(self, state: dict[str, Any]) -> None:
if "tgp_laser_code" not in state:

View File

@ -95,6 +95,13 @@ class FlightMembers(IFlightRoster):
# across all flight members.
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:
for member in self.members:
member.loadout = member.loadout.clone()

View File

@ -6,7 +6,7 @@ from typing import Type
from game.ato.flightplans.ibuilder import IBuilder
from game.ato.flightplans.patrolling import PatrollingFlightPlan, PatrollingLayout
from game.ato.flightplans.waypointbuilder import WaypointBuilder
from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles
from game.utils import Distance, Heading, Speed, knots, meters, nautical_miles
class AewcFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
@ -70,10 +70,7 @@ class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]):
builder = WaypointBuilder(self.flight)
if self.flight.unit_type.patrol_altitude is not None:
altitude = self.flight.unit_type.patrol_altitude
else:
altitude = feet(25000)
altitude = builder.get_patrol_altitude
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
@ -90,6 +87,7 @@ class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]):
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
custom_waypoints=list(),
)
def build(self, dump_debug_info: bool = False) -> AewcFlightPlan:

View File

@ -49,6 +49,7 @@ class AirAssaultLayout(FormationAttackLayout):
if self.divert is not None:
yield self.divert
yield self.bullseye
yield from self.custom_waypoints
class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
@ -111,12 +112,11 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
)
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)
altitude = builder.get_cruise_altitude
altitude_is_agl = self.flight.is_helo
if self.flight.is_hercules or self.flight.departure.cptype in [
ControlPointType.AIRCRAFT_CARRIER_GROUP,
ControlPointType.LHA_GROUP,
@ -133,13 +133,21 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
self._generate_ctld_pickup(),
)
)
pickup.alt = heli_alt
pickup.alt = altitude
pickup_position = pickup.position
ingress = builder.ingress(
FlightWaypointType.INGRESS_AIR_ASSAULT,
self.package.waypoints.ingress,
self.package.target,
ingress = (
builder.ingress(
FlightWaypointType.INGRESS_AIR_ASSAULT,
self.package.waypoints.ingress,
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)
@ -159,8 +167,6 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
drop_pos = tgt.position.point_from_heading(heading, 1200)
drop_off_zone = MissionTarget("Dropoff zone", drop_pos)
dz = builder.dropoff_zone(drop_off_zone) if self.flight.is_helo else None
if dz:
dz.alt = heli_alt
return AirAssaultLayout(
departure=builder.takeoff(self.flight.departure),
@ -184,9 +190,10 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
hold=None,
join=builder.join(ingress.position),
join=builder.join(self.package.waypoints.ingress),
split=builder.split(self.flight.arrival.position),
refuel=None,
custom_waypoints=list(),
)
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 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 .ibuilder import IBuilder
from .planningerror import PlanningError
@ -92,6 +92,7 @@ class AirliftLayout(StandardLayout):
if self.divert is not None:
yield self.divert
yield self.bullseye
yield from self.custom_waypoints
class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
@ -132,12 +133,11 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
"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)
altitude = builder.get_cruise_altitude
altitude_is_agl = self.flight.is_helo
pickup_ascent = None
pickup_descent = None
pickup = None
@ -246,6 +246,7 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
custom_waypoints=list(),
)
def build(self, dump_debug_info: bool = False) -> AirliftFlightPlan:

View File

@ -1,11 +1,10 @@
from __future__ import annotations
import random
from datetime import timedelta
from typing import Type
from game.theater import FrontLine
from game.utils import Distance, Speed, feet
from game.utils import Distance, Speed
from .capbuilder import CapBuilder
from .invalidobjectivelocation import InvalidObjectiveLocation
from .patrolling import PatrollingFlightPlan, PatrollingLayout
@ -41,14 +40,9 @@ class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
start_pos, end_pos = self.cap_racetrack_for_objective(location, barcap=True)
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
patrol_alt = max(
self.doctrine.min_patrol_altitude,
min(self.doctrine.max_patrol_altitude, randomized_alt),
)
builder = WaypointBuilder(self.flight)
patrol_alt = builder.get_patrol_altitude
start, end = builder.race_track(start_pos, end_pos, patrol_alt)
return PatrollingLayout(
@ -64,6 +58,7 @@ class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
custom_waypoints=list(),
)
def build(self, 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.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 .invalidobjectivelocation import InvalidObjectiveLocation
from .patrolling import PatrollingFlightPlan, PatrollingLayout
@ -37,6 +37,7 @@ class CasLayout(PatrollingLayout):
if self.divert is not None:
yield self.divert
yield self.bullseye
yield from self.custom_waypoints
class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay):
@ -104,11 +105,7 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]):
builder = WaypointBuilder(self.flight)
is_helo = self.flight.unit_type.dcs_unit_type.helicopter
ingress_egress_altitude = (
self.doctrine.ingress_altitude
if not is_helo
else feet(self.coalition.game.settings.heli_combat_alt_agl)
)
ingress_egress_altitude = builder.get_combat_altitude
use_agl_patrol_altitude = is_helo
ip_solver = IpSolver(
@ -167,6 +164,7 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]):
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
custom_waypoints=list(),
)
def build(self, dump_debug_info: bool = False) -> CasFlightPlan:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ from .formationattack import (
from .invalidobjectivelocation import InvalidObjectiveLocation
from .waypointbuilder import StrikeTarget
from ..flightwaypointtype import FlightWaypointType
from ...theater.theatergroup import SceneryUnit
class StrikeFlightPlan(FormationAttackFlightPlan):
@ -28,7 +29,10 @@ class Builder(FormationAttackBuilder[StrikeFlightPlan, FormationAttackLayout]):
targets: list[StrikeTarget] = []
for idx, unit in enumerate(location.strike_targets):
targets.append(StrikeTarget(f"{unit.type.id} #{idx}", unit))
name = unit.type.id
if isinstance(unit, SceneryUnit):
name = unit.name
targets.append(StrikeTarget(f"{name} #{idx}", unit))
return self._build(FlightWaypointType.INGRESS_STRIKE, targets)

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import logging
from collections.abc import Iterator
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, Tuple
from typing import Any, Dict, Tuple, Optional
import yaml
from packaging.version import Version
@ -19,8 +19,10 @@ from game.theater.iadsnetwork.iadsnetwork import IadsNetwork
from game.theater.theaterloader import TheaterLoader
from game.version import CAMPAIGN_FORMAT_VERSION
from .campaignairwingconfig import CampaignAirWingConfig
from .campaigncarrierconfig import CampaignCarrierConfig
from .campaigngroundconfig import TgoConfig
from .mizcampaignloader import MizCampaignLoader
from ..factions import FACTIONS, Faction
PERF_FRIENDLY = 0
PERF_MEDIUM = 1
@ -90,6 +92,16 @@ class Campaign:
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(
data["name"],
TheaterLoader(data["theater"].lower()).menu_thumbnail_dcs_relative_path,
@ -97,8 +109,8 @@ class Campaign:
data.get("authors", "???"),
data.get("description", ""),
(version.major, version.minor),
data.get("recommended_player_faction", "USA 2005"),
data.get("recommended_enemy_faction", "Russia 1990"),
player_faction,
enemy_faction,
start_date,
start_time,
data.get("recommended_player_money", DEFAULT_BUDGET),
@ -112,6 +124,19 @@ class Campaign:
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:
t = TheaterLoader(self.data["theater"].lower()).load()
@ -140,6 +165,13 @@ class Campaign:
return CampaignAirWingConfig({})
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:
ground_forces = self.data.get("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",
aircraft=aircraft,
livery=None,
livery_set=[],
auto_assignable_mission_types=set(aircraft.iter_task_capabilities()),
radio_presets={},
operating_bases=OperatingBases.default_for_aircraft(aircraft),

View File

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

View File

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

View File

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

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

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import itertools
import logging
from collections import defaultdict
from dataclasses import dataclass, field
from functools import cached_property
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING, Set
@ -93,11 +94,8 @@ class Faction:
# Required mods or asset packs
requirements: Dict[str, str] = field(default_factory=dict)
# Possible carrier names
carrier_names: Set[str] = field(default_factory=set)
# Possible helicopter carrier names
helicopter_carrier_names: Set[str] = field(default_factory=set)
# Possible carrier units mapped to names
carriers: Dict[ShipUnitType, Set[str]] = field(default_factory=dict)
# Available Naval Units
naval_units: Set[ShipUnitType] = field(default_factory=set)
@ -241,8 +239,30 @@ class Faction:
faction.requirements = json.get("requirements", {})
faction.carrier_names = json.get("carrier_names", [])
faction.helicopter_carrier_names = json.get("helicopter_carrier_names", [])
# First try to load the carriers in the new format which
# 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)
jtac_name = json.get("jtac_unit", None)
@ -340,6 +360,8 @@ class Faction:
if not mod_settings.f4bc_phantom:
self.remove_aircraft("VSN_F4B")
self.remove_aircraft("VSN_F4C")
if not mod_settings.f9f_panther:
self.remove_aircraft("VSN_F9F")
if not mod_settings.f15d_baz:
self.remove_aircraft("F-15D")
if not mod_settings.f_15_idf:
@ -381,6 +403,8 @@ class Faction:
self.remove_aircraft("JAS39Gripen")
self.remove_aircraft("JAS39Gripen_BVR")
self.remove_aircraft("JAS39Gripen_AG")
if not mod_settings.super_etendard:
self.remove_aircraft("VSN_SEM")
if not mod_settings.su30_flanker_h:
self.remove_aircraft("Su-30MKA")
self.remove_aircraft("Su-30MKI")
@ -592,3 +616,14 @@ def load_all_ships(data: list[str]) -> List[Type[ShipType]]:
if item is not None:
items.append(item)
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 typing import Dict, Iterator, List, Optional, Type
import yaml
from game import persistency
from game.factions.faction import Faction
@ -27,7 +29,11 @@ class FactionLoader:
@staticmethod
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
def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]:
@ -40,7 +46,10 @@ class FactionLoader:
for f in files:
try:
with f.open("r", encoding="utf-8") as fdata:
data = json.load(fdata)
if "yml" in f.name or "yaml" in f.name:
data = yaml.safe_load(fdata)
else:
data = json.load(fdata)
factions[data["name"]] = Faction.from_dict(data)
logging.info("Loaded faction : " + str(f))
except Exception:

View File

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

View File

@ -1,6 +1,7 @@
import logging
from typing import Any, Optional, Type, List
from dcs.point import MovingPoint
from dcs.task import (
AWACS,
AWACSTaskAction,
@ -31,8 +32,10 @@ from dcs.unitgroup import FlyingGroup
from game.ato import Flight, FlightType
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.theaterrefueling import TheaterRefuelingFlightPlan
from game.utils import nautical_miles
class AircraftBehavior:
@ -100,8 +103,25 @@ class AircraftBehavior:
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
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(OptReactOnThreat(react_on_threat))

View File

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

View File

@ -1,6 +1,6 @@
import logging
import random
from typing import Any, Union, Tuple, Optional
from typing import Any, Union, Tuple, Optional, List
from dcs import Mission
from dcs.country import Country
@ -19,7 +19,8 @@ from dcs.planes import (
)
from dcs.point import PointAction
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 (
FlyingGroup,
ShipGroup,
@ -110,24 +111,31 @@ class FlightGroupSpawner:
def create_idle_aircraft(self) -> Optional[FlyingGroup[Any]]:
group = None
if (
self.flight.is_helo
or self.flight.is_lha
and isinstance(self.flight.squadron.location, Fob)
):
cp = self.flight.squadron.location
if self.flight.is_helo or self.flight.is_lha and isinstance(cp, Fob):
group = self._generate_at_cp_helipad(
name=namegen.next_aircraft_name(self.country, self.flight),
cp=self.flight.squadron.location,
)
elif isinstance(self.flight.squadron.location, Fob):
elif isinstance(cp, Fob):
group = self._generate_at_cp_ground_spawn(
name=namegen.next_aircraft_name(self.country, self.flight),
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(
name=namegen.next_aircraft_name(self.country, self.flight),
airfield=self.flight.squadron.location,
airfield=cp,
parking_slots=slots,
)
if group:
group.uncontrolled = True
@ -196,7 +204,19 @@ class FlightGroupSpawner:
pad_group = self._generate_at_cp_ground_spawn(name, cp)
if pad_group is not None:
return pad_group
return self._generate_at_airfield(name, cp)
# 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)
else:
raise NotImplementedError(
f"Aircraft spawn behavior not implemented for {cp} ({cp.__class__})"
@ -210,6 +230,13 @@ class FlightGroupSpawner:
group = self._generate_over_departure(name, cp)
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]:
assert isinstance(self.flight.state, InFlight)
name = namegen.next_aircraft_name(self.country, self.flight)
@ -251,7 +278,12 @@ class FlightGroupSpawner:
group.points[0].alt_type = alt_type
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.
# 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
@ -268,7 +300,7 @@ class FlightGroupSpawner:
maintask=None,
start_type=self._start_type_at_airfield(airfield),
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_nr=self.flight.callsign.nr if self.flight.callsign else None,
)

View File

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

View File

@ -44,7 +44,16 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
task = AttackGroup(
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,
)
waypoint.tasks.append(task)

View File

@ -40,4 +40,4 @@ class HoldPointBuilder(PydcsWaypointBuilder):
if self.flight.is_helo:
waypoint.add_task(OptFormation.rotary_column())
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:
waypoint = super().build()
if self.ai_despawn(waypoint):
waypoint.alt = round(
self.flight.coalition.doctrine.max_patrol_altitude.meters
)
waypoint.alt = round(self.flight.unit_type.preferred_patrol_altitude.meters)
waypoint.alt_type = "BARO"
else:
waypoint.type = "Land"

View File

@ -124,7 +124,9 @@ class PydcsWaypointBuilder:
return False
def register_special_strike_points(
self, targets: Iterable[Union[MissionTarget, TheaterUnit]]
self,
targets: Iterable[Union[MissionTarget, TheaterUnit]],
start: int = 1,
) -> None:
"""Create special strike waypoints for various aircraft"""
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)
if self.group.units[0].unit_type == F_15ESE:
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:

View File

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

View File

@ -2,7 +2,7 @@ import copy
from typing import Union
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.task import Bombing, Expend, OptFormation, WeaponType, CarpetBombing
@ -62,13 +62,31 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
def add_strike_tasks(
self, waypoint: MovingPoint, weapon_type: WeaponType = WeaponType.Auto
) -> 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:
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:
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
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.speed = mach(0.85, meters(waypoint.alt)).meters_per_second
@ -76,4 +94,6 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
# Register special waypoints
if not self._special_wpts_injected:
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

View File

@ -12,7 +12,7 @@ from dcs.task import StartCommand
from dcs.triggers import Event, TriggerOnce, TriggerRule
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.flightwaypointtype import FlightWaypointType
from game.ato.starttype import StartType
@ -73,6 +73,18 @@ class WaypointGenerator:
if point.only_for_player and not self.flight.client_count:
continue
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:
# 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-

View File

@ -50,6 +50,7 @@ from game.weather.weather import Weather
from .aircraft.flightdata import FlightData
from .briefinggenerator import CommInfo, JtacInfo, MissionInfoGenerator
from .missiondata import AwacsInfo, TankerInfo
from ..persistency import kneeboards_dir
if TYPE_CHECKING:
from game import Game
@ -440,13 +441,14 @@ class BriefingPage(KneeboardPage):
sun = Sun(start_pos.lat, start_pos.lng)
date = fl.squadron.coalition.game.date
dt = datetime.datetime(date.year, date.month, date.day)
tz = fl.squadron.coalition.game.theater.timezone
# Get today's sunrise and sunset in UTC
sr_utc = sun.get_sunrise_time(date)
ss_utc = sun.get_sunset_time(date)
sr = sr_utc + tz.utcoffset(sun.get_sunrise_time(date))
ss = ss_utc + tz.utcoffset(sun.get_sunset_time(date))
sr_utc = sun.get_sunrise_time(dt)
ss_utc = sun.get_sunset_time(dt)
sr = sr_utc + tz.utcoffset(sun.get_sunrise_time(dt))
ss = ss_utc + tz.utcoffset(sun.get_sunset_time(dt))
writer.text(
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.write(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]]:
"""Returns a list of kneeboard pages per airframe in the mission.

View File

@ -39,7 +39,13 @@ from dcs.task import (
OptAlarmState,
)
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.unitgroup import MovingGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import ShipType, VehicleType
@ -404,14 +410,24 @@ class GroundObjectGenerator:
# is minimized. As long as the triggerzone is over the scenery object, we're ok.
smallest_valid_radius = feet(16).meters
trigger_zone = self.m.triggers.add_triggerzone(
scenery.zone.position,
smallest_valid_radius,
scenery.zone.hidden,
scenery.zone.name,
color,
scenery.zone.properties,
)
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(
scenery.zone.position,
smallest_valid_radius,
scenery.zone.hidden,
scenery.zone.name,
color,
scenery.zone.properties,
)
# DCS only visually shows a scenery object is dead when
# this trigger rule is applied. Otherwise you can kill a
# structure twice.

View File

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

View File

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

View File

@ -314,8 +314,18 @@ class Settings:
page=CAMPAIGN_DOCTRINE_PAGE,
section=GENERAL_SECTION,
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 (nmi)",
page=CAMPAIGN_DOCTRINE_PAGE,
@ -388,6 +398,32 @@ class Settings:
"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
ai_pilot_levelling: bool = boolean_option(
"Allow AI pilot leveling",
@ -720,6 +756,16 @@ class Settings:
"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
desired_player_mission_duration: timedelta = minutes_option(
"Desired mission duration",
@ -1150,6 +1196,7 @@ class Settings:
enable_transfer_cheat: bool = False
enable_runway_state_cheat: bool = False
enable_air_wing_adjustments: bool = False
enable_enemy_buy_sell: bool = False
# LUA Plugins system
plugins: Dict[str, bool] = field(default_factory=dict)

View File

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

View File

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

View File

@ -27,6 +27,7 @@ class SquadronDef:
role: str
aircraft: AircraftType
livery: Optional[str]
livery_set: list[str]
auto_assignable_mission_types: set[FlightType]
radio_presets: dict[Union[str, int], list[RadioFrequency]]
operating_bases: OperatingBases
@ -103,6 +104,7 @@ class SquadronDef:
role=data["role"],
aircraft=unit_type,
livery=data.get("livery"),
livery_set=data.get("livery_set", []),
auto_assignable_mission_types=set(unit_type.iter_task_capabilities()),
radio_presets=radio_presets,
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.
self.ground_unit_orders.process(game, game.conditions.start_time)
self.release_parking_slots()
runway_status = self.runway_status
if runway_status is not None:
runway_status.process_turn()
@ -1393,6 +1395,11 @@ class NavalControlPoint(
L02,
L52,
L61,
CV_1143_5,
CVN_71,
CVN_72,
CVN_73,
CVN_75,
]:
return True
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
skynet_node = SkynetNode.from_group(node.group)
for connection in node.connections.values():
if not any([x.alive for x in connection.units]):
continue
if connection.ground_object.is_friendly(
skynet_node.player
) 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
import dcs.statics
from dcs.countries import country_dict
from game import Game
from game.factions.faction import Faction
from game.naming import namegen
from game.scenery_group import SceneryGroup
from game.theater import PointWithHeading, PresetLocation
from game.theater import PointWithHeading, PresetLocation, NavalControlPoint
from game.theater.theatergroundobject import (
BuildingGroundObject,
IadsBuildingGroundObject,
@ -26,12 +27,21 @@ from . import (
Fob,
OffMapSpawn,
)
from .theatergroup import IadsGroundGroup, IadsRole, SceneryUnit, TheaterGroup
from .theatergroup import (
IadsGroundGroup,
IadsRole,
SceneryUnit,
TheaterGroup,
TheaterUnit,
)
from ..armedforces.armedforces import ArmedForces
from ..armedforces.forcegroup import ForceGroup
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
from ..campaignloader.campaigncarrierconfig import CampaignCarrierConfig
from ..campaignloader.campaigngroundconfig import TgoConfig
from ..data.groups import GroupTask
from ..data.units import UnitClass
from ..dcs.shipunittype import ShipUnitType
from ..profiling import logged_duration
from ..settings import Settings
@ -49,6 +59,7 @@ class GeneratorSettings:
no_player_navy: bool
no_enemy_navy: bool
tgo_config: TgoConfig
carrier_config: CampaignCarrierConfig
squadrons_start_full: bool
@ -58,6 +69,7 @@ class ModSettings:
a6a_intruder: bool = False
a7e_corsair2: bool = False
f4bc_phantom: bool = False
f9f_panther: bool = False
f15d_baz: bool = False
f_15_idf: bool = False
f_16_idf: bool = False
@ -72,6 +84,7 @@ class ModSettings:
irondome: bool = False
uh_60l: bool = False
jas39_gripen: bool = False
super_etendard: bool = False
su30_flanker_h: bool = False
su57_felon: bool = False
frenchpack: bool = False
@ -125,11 +138,13 @@ class GameGenerator:
def should_remove_carrier(self, player: bool) -> bool:
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:
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:
to_remove: List[ControlPoint] = []
@ -221,14 +236,63 @@ class GenericCarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
carrier = next(self.control_point.ground_objects[-1].units)
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):
def generate(self) -> bool:
if not super().generate():
return False
carrier_names = self.faction.carrier_names
if not carrier_names:
carriers = self.faction.carriers
if not carriers:
logging.info(
f"Skipping generation of {self.control_point.name} because "
f"{self.faction_name} has no carriers"
@ -239,6 +303,7 @@ class CarrierGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
if not unit_group:
logging.error(f"{self.faction_name} has no access to AircraftCarrier")
return False
self.generate_ground_object_from_group(
unit_group,
PresetLocation(
@ -248,7 +313,7 @@ class CarrierGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
),
GroupTask.AIRCRAFT_CARRIER,
)
self.update_carrier_name(random.choice(list(carrier_names)))
self.apply_carrier_config()
return True
@ -257,8 +322,8 @@ class LhaGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
if not super().generate():
return False
lha_names = self.faction.helicopter_carrier_names
if not lha_names:
lhas = self.faction.carriers
if not lhas:
logging.info(
f"Skipping generation of {self.control_point.name} because "
f"{self.faction_name} has no LHAs"
@ -280,7 +345,7 @@ class LhaGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
),
GroupTask.HELICOPTER_CARRIER,
)
self.update_carrier_name(random.choice(list(lha_names)))
self.apply_carrier_config()
return True

View File

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

View File

@ -2,6 +2,7 @@ from .SWPack import *
from .a4ec import *
from .a7e import *
from .a6a import *
from .f9f import *
from .f100 import *
from .f104 import *
from .f105 import *
@ -20,6 +21,7 @@ from .irondome import *
from .jas39 import *
from .ov10a import *
from .spanishnavypack import *
from .super_etendard import *
from .su30 import *
from .su57 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
try:
liberation_install.replace_mission_scripting_file()
except:
except Exception as e:
logging.error(e)
error_dialog = QtWidgets.QErrorMessage()
error_dialog.setWindowTitle("Wrong DCS installation directory.")
error_dialog.showMessage(
@ -317,6 +318,7 @@ def create_game(
no_player_navy=False,
no_enemy_navy=False,
tgo_config=campaign.load_ground_forces_config(),
carrier_config=campaign.load_carrier_config(),
),
ModSettings(
a4_skyhawk=False,
@ -325,6 +327,7 @@ def create_game(
fa_18efg=False,
fa18ef_tanker=False,
f4bc_phantom=False,
f9f_panther=False,
f22_raptor=False,
f84g_thunderjet=False,
f100_supersabre=False,

View File

@ -141,6 +141,7 @@ class QFlightList(QListView):
)
return
self.package_model.add_flight(clone)
EventStream.put_nowait(GameUpdateEvents().new_flight(clone))
def cancel_or_abort_flight(self, index: QModelIndex) -> None:
self.package_model.cancel_or_abort_flight_at_index(index)
@ -338,6 +339,10 @@ class QPackageList(QListView):
)
return
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:
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}"
else:
wpt.description = f"Enemy unit: {target.name}"
wpt.waypoint_type = FlightWaypointType.TARGET_POINT
i = add_model_item(i, model, wpt.pretty_name, wpt)
if self.include_airbases:

View File

@ -109,7 +109,7 @@ class QLiberationWindow(QMainWindow):
if last_save_file:
logging.info("Loading last saved game : " + str(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.updateWindowTitle(last_save_file if game else None)
else:
@ -342,7 +342,7 @@ class QLiberationWindow(QMainWindow):
)
if file is not None and 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)
self.updateWindowTitle(file[0])
@ -350,15 +350,23 @@ class QLiberationWindow(QMainWindow):
def migrate_game(self, game, path):
if game:
is_liberation = ".liberation" in path
Migrator(game, is_liberation)
try:
Migrator(game, is_liberation)
return game
except Exception:
self.incompatible_save_popup(path)
else:
relative_path = Path(path)
QMessageBox.critical(
self,
"Incompatible save",
"Incompatible save file detected, please report the issue on GitHub or Discord.\n"
f"Make sure to include the campaign that fails to load, i.e.:\n\n{relative_path}",
)
self.incompatible_save_popup(path)
return None
def incompatible_save_popup(self, path):
relative_path = Path(path)
QMessageBox.critical(
self,
"Incompatible save",
"Incompatible save file detected, please report the issue on GitHub or Discord.\n"
f"Make sure to include the campaign that fails to load, i.e.:\n\n{relative_path}",
)
def saveGame(self):
logging.info("Saving game")

View File

@ -239,5 +239,7 @@ class QWaitingForMissionResultWindow(QDialog):
self.sim_controller.set_game(self.game)
for _, f in self.game.db.flights.objects.items():
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)
self.close()

View File

@ -32,6 +32,7 @@ from game.theater.theatergroundobject import (
EwrGroundObject,
SamGroundObject,
VehicleGroupGroundObject,
ShipGroundObject,
)
from qt_ui.uiconstants import EVENT_ICONS
@ -188,7 +189,8 @@ class QGroundObjectTemplateLayout(QGroupBox):
@property
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(
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)
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 = []
for group_name, groups in self.layout_model.groups.items():
for group in groups:
@ -276,13 +279,17 @@ class QGroundObjectBuyMenu(QDialog):
elif isinstance(ground_object, EwrGroundObject):
role = GroupRole.AIR_DEFENSE
tasks.append(GroupTask.EARLY_WARNING_RADAR)
elif isinstance(ground_object, ShipGroundObject):
role = GroupRole.NAVAL
tasks.append(GroupTask.NAVY)
else:
raise NotImplementedError(f"Unhandled TGO type {ground_object.__class__}")
if not 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.setEnabled(self.force_group_selector.count() > 1)
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.setProperty("style", "btn-success")
if self.ground_object.purchasable:
if self.ground_object.purchasable or self.game.turn == 0:
if self.total_value > 0:
self.actionLayout.addWidget(self.sell_all_button)
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.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):
self.update_total_value()
self.intelBox = QGroupBox("Units :")
@ -240,7 +248,7 @@ class QGroundObjectMenu(QDialog):
self.actionLayout.addWidget(self.sell_all_button)
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)
except Exception as e:
logging.exception(e)
@ -276,7 +284,8 @@ class QGroundObjectMenu(QDialog):
def sell_all(self):
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.update_game()
@ -296,7 +305,10 @@ class QGroundObjectMenu(QDialog):
for package in self.game.ato_for(player=False).packages
):
# 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)
GameUpdateSignal.get_instance().updateGame(self.game)
# Refresh the dialog

View File

@ -223,7 +223,7 @@ class QAutoCreateDialog(QDialog):
self.game.settings,
)
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:
package.set_tot_asap(now)
self.package = package

View File

@ -17,6 +17,7 @@ from game.ato.flight import Flight
from game.ato.flightmember import FlightMember
from game.ato.loadouts import Loadout
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
from qt_ui.widgets.combos.QSquadronLiverySelector import SquadronLiverySelector
from .QLoadoutEditor import QLoadoutEditor
from .ownlasercodeinfo import OwnLaserCodeInfo
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()
scrolling_layout = QVBoxLayout()
scroll_content.setLayout(scrolling_layout)
@ -212,6 +231,9 @@ class QFlightPayloadTab(QFrame):
self.property_editor.set_flight_member(member)
self.loadout_selector.setCurrentText(member.loadout.name)
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.weapon_laser_code_selector.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.flight.use_same_loadout_for_all_members
)
self.livery_selector.setDisabled(
self.flight.use_same_livery_for_all_members
)
else:
self.loadout_selector.setEnabled(True)
self.payload_editor.setEnabled(True)
self.livery_selector.setEnabled(True)
def loadout_at(self, index: int) -> Loadout:
loadout = self.loadout_selector.itemData(index)
@ -273,3 +299,20 @@ class QFlightPayloadTab(QFrame):
self.rebind_to_selected_member()
else:
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))
altitude = int(waypoint.alt.feet)
altitude = round(waypoint.alt.feet)
altitude_item = QStandardItem(f"{altitude}")
altitude_item.setEditable(True)
self.model.setItem(row, 1, altitude_item)

View File

@ -162,9 +162,9 @@ class QFlightWaypointTab(QFrame):
assert isinstance(self.flight.flight_plan, CustomFlightPlan)
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(
self,
parent if parent else self,
"Degrade 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>"
@ -184,12 +184,6 @@ class QFlightWaypointTab(QFrame):
def on_waypoints_added(self, waypoints: Iterable[FlightWaypoint]) -> None:
if not waypoints:
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.add_rows(len(list(waypoints)))

View File

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

View File

@ -100,6 +100,8 @@ class GeneratorOptions(QtWidgets.QWizardPage):
self.registerField("uh_60l", self.uh_60l)
self.f4bc_phantom = QtWidgets.QCheckBox()
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.registerField("f15d_baz", self.f15d_baz)
self.f_15_idf = QtWidgets.QCheckBox()
@ -122,6 +124,8 @@ class GeneratorOptions(QtWidgets.QWizardPage):
self.registerField("f105_thunderchief", self.f105_thunderchief)
self.jas39_gripen = QtWidgets.QCheckBox()
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.registerField("su30_flanker_h", self.su30_flanker_h)
self.su57_felon = QtWidgets.QCheckBox()
@ -150,6 +154,7 @@ class GeneratorOptions(QtWidgets.QWizardPage):
modLayout_row = 1
mod_pairs = [
("F9F Panther (v2.8.7.101)", self.f9f_panther),
("A-4E Skyhawk (v2.2.0)", self.a4_skyhawk),
("A-6A Intruder (v2.7.5.01)", self.a6a_intruder),
("A-7E Corsair II", self.a7e_corsair2),
@ -158,7 +163,7 @@ class GeneratorOptions(QtWidgets.QWizardPage):
("F-15D Baz (v1.0)", self.f15d_baz),
("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/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-22A Raptor", self.f22_raptor),
("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),
("JAS 39 Gripen (v1.8.5-beta)", self.jas39_gripen),
("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-57 Felon (build-04)", self.su57_felon),
("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.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.a6a_intruder.setChecked(s.get("a6a_intruder", 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.f105_thunderchief.setChecked(s.get("f105_thunderchief", 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.su57_felon.setChecked(s.get("su57_felon", 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)
# 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
def show_red_ato(self) -> bool:
return self.red_ato_checkbox.isChecked()
@ -137,6 +146,10 @@ class CheatSettingsBox(QGroupBox):
def enable_air_wing_cheats(self) -> bool:
return self.air_wing_adjustments_checkbox.isChecked()
@property
def enable_redfor_buysell(self) -> bool:
return self.opfor_buysell_checkbox.isChecked()
class AutoSettingsLayout(QGridLayout):
def __init__(
@ -495,6 +508,7 @@ class QSettingsWidget(QtWidgets.QWizardPage, SettingsContainer):
self.settings.enable_air_wing_adjustments = (
self.cheat_options.enable_air_wing_cheats
)
self.settings.enable_enemy_buy_sell = self.cheat_options.enable_redfor_buysell
if self.game:
events = GameUpdateEvents()
@ -521,6 +535,15 @@ class QSettingsWidget(QtWidgets.QWizardPage, SettingsContainer):
self.cheat_options.transfer_cheat_checkbox.setChecked(
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.pluginsOptionsPage.update_from_settings()

View File

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

View File

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

View File

@ -17,7 +17,7 @@ description:
adversarial nation, dubbed Orangeland. This scenario is designed to
simulate a mock invasion against Cairo, and presents a valuable
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
Israel as an observer nation. This marks a significant milestone, given
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,
not only demonstrates the broadening scope of the exercise but also highlights
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
performance: 1
recommended_start_date: 2025-09-01
version: "10.7"
settings:
hercules: true
squadrons:
Blue CV-1:
- primary: SEAD
@ -45,7 +51,7 @@ squadrons:
aircraft:
- S-3B Tanker
size: 4
Bombers from RAF Fairford:
Bombers from Al Udeid Air Base:
- primary: Anti-ship
secondary: air-to-ground
aircraft:
@ -56,8 +62,13 @@ squadrons:
aircraft:
- B-1B Lancer
size: 8
# Hatzerim (141)
7:
# Ovda
10:
- primary: CAS
secondary: air-to-ground
aircraft:
- A-10C Thunderbolt II (Suite 7)
size: 8
- primary: Escort
secondary: any
aircraft:
@ -68,36 +79,47 @@ squadrons:
aircraft:
- F-15E Strike Eagle (Suite 4+)
size: 16
- primary: BAI
secondary: any
aircraft:
- JF-17 Thunder
size: 12
# Ramon Airbase
9:
- primary: DEAD
secondary: any
aircraft:
- F-16CM Fighting Falcon (Block 50)
size: 20
- primary: BAI
secondary: any
aircraft:
- JF-17 Thunder
size: 16
- primary: BARCAP
secondary: any
aircraft:
- Mirage 2000C
size: 12
# Kedem
12:
- primary: Transport
secondary: any
aircraft:
- CH-47D
size: 20
size: 12
- primary: BAI
secondary: any
aircraft:
- AH-64D Apache Longbow
size: 8
- primary: Air Assault
secondary: any
aircraft:
- UH-60L
- UH-60A
size: 4
# Nevatim (106)
8:
# Ben-Gurion
24:
- primary: Transport
secondary: any
aircraft:
- C-130J-30 Super Hercules
- C-130
size: 8
- primary: AEW&C
aircraft:
- E-3A
@ -106,16 +128,12 @@ squadrons:
aircraft:
- KC-135 Stratotanker
size: 2
- primary: CAS
secondary: air-to-ground
aircraft:
- A-10C Thunderbolt II (Suite 7)
size: 8
# Melez (30)
5:
- primary: CAS
secondary: air-to-ground
secondary: any
aircraft:
- Ka-50 Hokum III
- Ka-50 Hokum (Blackshark 3)
size: 4
- primary: BAI
@ -140,7 +158,17 @@ squadrons:
- F-4E Phantom II
size: 20
- primary: DEAD
secondary: any
secondary:
- BAI
- BARCAP
- CAS
- Escort
- Fighter sweep
- Intercept
- OCA/Aircraft
- OCA/Runway
- Strike
- TARCAP
aircraft:
- F-16CM Fighting Falcon (Block 50)
size: 20
@ -157,7 +185,9 @@ squadrons:
# Cairo West (95)
18:
- primary: Transport
secondary: any
aircraft:
- C-130J-30 Super Hercules
- C-130
size: 8
- primary: BARCAP

View File

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

View File

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

View File

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

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