From 198ff7d8a3804d5403f1cd76961d629736d06eea Mon Sep 17 00:00:00 2001 From: Raffson Date: Mon, 29 May 2023 00:11:27 +0200 Subject: [PATCH] Allow deletion of certain types of waypoints Resolves #60 NAV/REFUEL/DIVERT waypoints should have no effect on the timings. --- changelog.md | 1 + game/ato/flightplans/airassault.py | 4 ++-- game/ato/flightplans/airlift.py | 18 +++++++++++++-- game/ato/flightplans/cas.py | 2 +- game/ato/flightplans/custom.py | 2 +- game/ato/flightplans/ferry.py | 10 +++++++- game/ato/flightplans/flightplan.py | 5 +++- game/ato/flightplans/formation.py | 20 +++++++++++++--- game/ato/flightplans/formationattack.py | 4 ++-- game/ato/flightplans/loiter.py | 2 +- game/ato/flightplans/patrolling.py | 13 ++++++++++- game/ato/flightplans/rtb.py | 2 +- game/ato/flightplans/standard.py | 8 ++++++- game/ato/flightplans/sweep.py | 2 +- game/ato/flightplans/tarcap.py | 2 +- game/ato/flightplans/waypointbuilder.py | 2 +- .../flight/waypoints/QFlightWaypointTab.py | 23 +++++++++++++++---- 17 files changed, 96 insertions(+), 24 deletions(-) diff --git a/changelog.md b/changelog.md index 0f074013..4b7ffd0e 100644 --- a/changelog.md +++ b/changelog.md @@ -16,6 +16,7 @@ * **[Modding]** Support for Iron Dome v1.2 by IDF Mods Project * **[New Game Wizard]** Re-organized generator options & show the regular settings menu instead of the limited "Difficulty & Automation" page. * **[Campaign Management]** Ability to operate harriers from FOBs/FARPs for __human pilots only__. Please note that the autoplanner won't generate flights for harriers at FOBs/FARPs, which means you need to plan your missions manually. +* **[Mission Planning]** Allow NAV/REFUEL/DIVERT waypoints to be deleted without degrading to a custom flight-plan, also warning the user before actually degrading the flight-plan. ## Fixes * **[New Game Wizard]** Settings would not persist when going back to a previous page (obsolete due to overhaul). diff --git a/game/ato/flightplans/airassault.py b/game/ato/flightplans/airassault.py index f4d373de..2a85b698 100644 --- a/game/ato/flightplans/airassault.py +++ b/game/ato/flightplans/airassault.py @@ -4,11 +4,11 @@ from dataclasses import dataclass from datetime import timedelta from typing import Iterator, TYPE_CHECKING, Type -from ._common_ctld import generate_random_ctld_point from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout from game.theater.controlpoint import ControlPointType from game.theater.missiontarget import MissionTarget from game.utils import Distance, feet, meters +from ._common_ctld import generate_random_ctld_point from .ibuilder import IBuilder from .planningerror import PlanningError from .uizonedisplay import UiZone, UiZoneDisplay @@ -21,7 +21,7 @@ if TYPE_CHECKING: from ..flightwaypoint import FlightWaypoint -@dataclass(frozen=True) +@dataclass class AirAssaultLayout(StandardLayout): # The pickup point is optional because we don't always need to load the cargo. When # departing from a carrier, LHA, or off-map spawn, the cargo is pre-loaded. diff --git a/game/ato/flightplans/airlift.py b/game/ato/flightplans/airlift.py index 17516f3b..c4f8d53c 100644 --- a/game/ato/flightplans/airlift.py +++ b/game/ato/flightplans/airlift.py @@ -5,9 +5,9 @@ from dataclasses import dataclass from datetime import timedelta from typing import TYPE_CHECKING, Type -from ._common_ctld import generate_random_ctld_point from game.theater.missiontarget import MissionTarget from game.utils import feet +from ._common_ctld import generate_random_ctld_point from .ibuilder import IBuilder from .planningerror import PlanningError from .standard import StandardFlightPlan, StandardLayout @@ -19,7 +19,7 @@ if TYPE_CHECKING: from dcs import Point -@dataclass(frozen=True) +@dataclass class AirliftLayout(StandardLayout): nav_to_pickup: list[FlightWaypoint] # There will not be a pickup waypoint when the pickup airfield is the departure @@ -38,6 +38,20 @@ class AirliftLayout(StandardLayout): ctld_drop_off_zone: FlightWaypoint | None nav_to_home: list[FlightWaypoint] + def delete_waypoint(self, waypoint: FlightWaypoint) -> bool: + if super().delete_waypoint(waypoint): + return True + if waypoint in self.nav_to_pickup: + self.nav_to_pickup.remove(waypoint) + return True + elif waypoint in self.nav_to_drop_off: + self.nav_to_drop_off.remove(waypoint) + return True + elif waypoint in self.nav_to_home: + self.nav_to_home.remove(waypoint) + return True + return False + def iter_waypoints(self) -> Iterator[FlightWaypoint]: yield self.departure yield from self.nav_to_pickup diff --git a/game/ato/flightplans/cas.py b/game/ato/flightplans/cas.py index 1879612d..0423b1ca 100644 --- a/game/ato/flightplans/cas.py +++ b/game/ato/flightplans/cas.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: from ..flightwaypoint import FlightWaypoint -@dataclass(frozen=True) +@dataclass class CasLayout(PatrollingLayout): target: FlightWaypoint diff --git a/game/ato/flightplans/custom.py b/game/ato/flightplans/custom.py index 35caa8e0..c47e93c5 100644 --- a/game/ato/flightplans/custom.py +++ b/game/ato/flightplans/custom.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: from ..flightwaypoint import FlightWaypoint -@dataclass(frozen=True) +@dataclass class CustomLayout(Layout): custom_waypoints: list[FlightWaypoint] diff --git a/game/ato/flightplans/ferry.py b/game/ato/flightplans/ferry.py index 0ddd82ac..de55cced 100644 --- a/game/ato/flightplans/ferry.py +++ b/game/ato/flightplans/ferry.py @@ -15,10 +15,18 @@ if TYPE_CHECKING: from ..flightwaypoint import FlightWaypoint -@dataclass(frozen=True) +@dataclass class FerryLayout(StandardLayout): nav_to_destination: list[FlightWaypoint] + def delete_waypoint(self, waypoint: FlightWaypoint) -> bool: + if super().delete_waypoint(waypoint): + return True + if waypoint in self.nav_to_destination: + self.nav_to_destination.remove(waypoint) + return True + return False + def iter_waypoints(self) -> Iterator[FlightWaypoint]: yield self.departure yield from self.nav_to_destination diff --git a/game/ato/flightplans/flightplan.py b/game/ato/flightplans/flightplan.py index 99eb45ef..76e9c5c5 100644 --- a/game/ato/flightplans/flightplan.py +++ b/game/ato/flightplans/flightplan.py @@ -41,7 +41,7 @@ INGRESS_TYPES = { } -@dataclass(frozen=True) +@dataclass class Layout(ABC): departure: FlightWaypoint @@ -50,6 +50,9 @@ class Layout(ABC): """A list of all waypoints in the flight plan, in order.""" return list(self.iter_waypoints()) + def delete_waypoint(self, waypoint: FlightWaypoint) -> bool: + return False + def iter_waypoints(self) -> Iterator[FlightWaypoint]: """Iterates over all waypoints in the flight plan, in order.""" raise NotImplementedError diff --git a/game/ato/flightplans/formation.py b/game/ato/flightplans/formation.py index 5aa39263..cf5bc1a8 100644 --- a/game/ato/flightplans/formation.py +++ b/game/ato/flightplans/formation.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from datetime import timedelta from functools import cached_property -from typing import Any, TYPE_CHECKING, TypeGuard +from typing import Any, TYPE_CHECKING, TypeGuard, Optional from game.typeguard import self_type_guard from game.utils import Speed @@ -16,14 +16,28 @@ if TYPE_CHECKING: from ..flightwaypoint import FlightWaypoint -@dataclass(frozen=True) +@dataclass class FormationLayout(LoiterLayout, ABC): nav_to: list[FlightWaypoint] join: FlightWaypoint split: FlightWaypoint - refuel: FlightWaypoint + refuel: Optional[FlightWaypoint] nav_from: list[FlightWaypoint] + def delete_waypoint(self, waypoint: FlightWaypoint) -> bool: + if super().delete_waypoint(waypoint): + return True + if waypoint in self.nav_to: + self.nav_to.remove(waypoint) + return True + elif waypoint in self.nav_from: + self.nav_from.remove(waypoint) + return True + elif waypoint == self.refuel: + self.refuel = None + return True + return False + class FormationFlightPlan(LoiterFlightPlan, ABC): @property diff --git a/game/ato/flightplans/formationattack.py b/game/ato/flightplans/formationattack.py index 435c0c06..fc930088 100644 --- a/game/ato/flightplans/formationattack.py +++ b/game/ato/flightplans/formationattack.py @@ -10,7 +10,7 @@ from dcs import Point from game.flightplan import HoldZoneGeometry from game.theater import MissionTarget -from game.utils import Speed, meters, Distance +from game.utils import Speed, meters from .flightplan import FlightPlan from .formation import FormationFlightPlan, FormationLayout from .ibuilder import IBuilder @@ -130,7 +130,7 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC): return super().tot_for_waypoint(waypoint) -@dataclass(frozen=True) +@dataclass class FormationAttackLayout(FormationLayout): ingress: FlightWaypoint targets: list[FlightWaypoint] diff --git a/game/ato/flightplans/loiter.py b/game/ato/flightplans/loiter.py index c0e4ce56..c3545dcd 100644 --- a/game/ato/flightplans/loiter.py +++ b/game/ato/flightplans/loiter.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: from ..flightwaypoint import FlightWaypoint -@dataclass(frozen=True) +@dataclass class LoiterLayout(StandardLayout, ABC): hold: FlightWaypoint diff --git a/game/ato/flightplans/patrolling.py b/game/ato/flightplans/patrolling.py index eecc1828..b4815188 100644 --- a/game/ato/flightplans/patrolling.py +++ b/game/ato/flightplans/patrolling.py @@ -16,13 +16,24 @@ if TYPE_CHECKING: from .flightplan import FlightPlan -@dataclass(frozen=True) +@dataclass class PatrollingLayout(StandardLayout): nav_to: list[FlightWaypoint] patrol_start: FlightWaypoint patrol_end: FlightWaypoint nav_from: list[FlightWaypoint] + def delete_waypoint(self, waypoint: FlightWaypoint) -> bool: + if super().delete_waypoint(waypoint): + return True + if waypoint in self.nav_to: + self.nav_to.remove(waypoint) + return True + elif waypoint in self.nav_from: + self.nav_from.remove(waypoint) + return True + return False + def iter_waypoints(self) -> Iterator[FlightWaypoint]: yield self.departure yield from self.nav_to diff --git a/game/ato/flightplans/rtb.py b/game/ato/flightplans/rtb.py index 8f1cbfa8..b5fd5a11 100644 --- a/game/ato/flightplans/rtb.py +++ b/game/ato/flightplans/rtb.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: from ..flightwaypoint import FlightWaypoint -@dataclass(frozen=True) +@dataclass class RtbLayout(StandardLayout): abort_location: FlightWaypoint nav_to_destination: list[FlightWaypoint] diff --git a/game/ato/flightplans/standard.py b/game/ato/flightplans/standard.py index 5cc45f95..89c5354c 100644 --- a/game/ato/flightplans/standard.py +++ b/game/ato/flightplans/standard.py @@ -10,12 +10,18 @@ if TYPE_CHECKING: from ..flightwaypoint import FlightWaypoint -@dataclass(frozen=True) +@dataclass class StandardLayout(Layout, ABC): arrival: FlightWaypoint divert: FlightWaypoint | None bullseye: FlightWaypoint + def delete_waypoint(self, waypoint: FlightWaypoint) -> bool: + if waypoint is self.divert: + self.divert = None + return True + return False + LayoutT = TypeVar("LayoutT", bound=StandardLayout) diff --git a/game/ato/flightplans/sweep.py b/game/ato/flightplans/sweep.py index caa26487..6e09b7da 100644 --- a/game/ato/flightplans/sweep.py +++ b/game/ato/flightplans/sweep.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: from ..flightwaypoint import FlightWaypoint -@dataclass(frozen=True) +@dataclass class SweepLayout(LoiterLayout): nav_to: list[FlightWaypoint] sweep_start: FlightWaypoint diff --git a/game/ato/flightplans/tarcap.py b/game/ato/flightplans/tarcap.py index 560ef0d7..1a43ce81 100644 --- a/game/ato/flightplans/tarcap.py +++ b/game/ato/flightplans/tarcap.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: from ..flightwaypoint import FlightWaypoint -@dataclass(frozen=True) +@dataclass class TarCapLayout(PatrollingLayout): refuel: FlightWaypoint | None diff --git a/game/ato/flightplans/waypointbuilder.py b/game/ato/flightplans/waypointbuilder.py index 93fc9fdd..f1ddd38a 100644 --- a/game/ato/flightplans/waypointbuilder.py +++ b/game/ato/flightplans/waypointbuilder.py @@ -437,7 +437,7 @@ class WaypointBuilder: return FlightWaypoint( "SEAD Search", - FlightWaypointType.CUSTOM, + FlightWaypointType.INGRESS_SEAD, hold, self.doctrine.ingress_altitude, alt_type="BARO", diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py index bfae6686..f075ffc0 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py @@ -109,13 +109,28 @@ class QFlightWaypointTab(QFrame): # Need to degrade to a custom flight plan and remove the waypoint. # If the waypoint is a target waypoint and is not the last target # waypoint, we don't need to degrade. - if isinstance(self.flight.flight_plan, FormationAttackFlightPlan): - is_target = waypoint in self.flight.flight_plan.target_area_waypoint.targets - count = len(self.flight.flight_plan.target_area_waypoint.targets) + fp = self.flight.flight_plan + if isinstance(fp, FormationAttackFlightPlan): + is_target = waypoint in fp.target_area_waypoint.targets + count = len(fp.target_area_waypoint.targets) if is_target and count > 1: - self.flight.flight_plan.target_area_waypoint.targets.remove(waypoint) + fp.target_area_waypoint.targets.remove(waypoint) return + if fp.layout.delete_waypoint(waypoint): + return + + result = QMessageBox.warning( + 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.

" + "Are you sure you wish to continue?", + QMessageBox.Yes, + QMessageBox.No, + ) + if result == QMessageBox.No: + return self.degrade_to_custom_flight_plan() assert isinstance(self.flight.flight_plan, CustomFlightPlan) self.flight.flight_plan.layout.custom_waypoints.remove(waypoint)