diff --git a/changelog.md b/changelog.md index bf490711..1c2be905 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,7 @@ Saves from 8.x are not compatible with 9.0.0. * **[Data]** Added support for the ARA Veinticinco de Mayo. * **[Data]** Changed display name of the AI-only F-15E Strike Eagle for clarity. * **[Flight Planning]** Improved IP selection for targets that are near the center of a threat zone. +* **[Flight Planning]** Moved CAS ingress point off the front line so that the AI begins their target search earlier. * **[Flight Planning]** Loadouts and aircraft properties can now be set per-flight member. Warning: AI flights should not use mixed loadouts. * **[Flight Planning]** Laser codes that are pre-assigned to weapons at mission start can now be chosen from a list in the loadout UI. This does not affect the aircraft's TGP, just the weapons. Currently only implemented for the F-15E S4+ and F-16C. * **[Mission Generation]** Configured target and initial points for F-15E S4+. diff --git a/game/ato/flightplans/aewc.py b/game/ato/flightplans/aewc.py index 6da848e5..7534ce32 100644 --- a/game/ato/flightplans/aewc.py +++ b/game/ato/flightplans/aewc.py @@ -90,5 +90,5 @@ class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]): bullseye=builder.bullseye(), ) - def build(self) -> AewcFlightPlan: + def build(self, dump_debug_info: bool = False) -> AewcFlightPlan: return AewcFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/airassault.py b/game/ato/flightplans/airassault.py index 721323a2..ac37640b 100644 --- a/game/ato/flightplans/airassault.py +++ b/game/ato/flightplans/airassault.py @@ -152,5 +152,5 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]): bullseye=builder.bullseye(), ) - def build(self) -> AirAssaultFlightPlan: + def build(self, dump_debug_info: bool = False) -> AirAssaultFlightPlan: return AirAssaultFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/airlift.py b/game/ato/flightplans/airlift.py index 39139b41..d62c4e7a 100644 --- a/game/ato/flightplans/airlift.py +++ b/game/ato/flightplans/airlift.py @@ -155,5 +155,5 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]): bullseye=builder.bullseye(), ) - def build(self) -> AirliftFlightPlan: + def build(self, dump_debug_info: bool = False) -> AirliftFlightPlan: return AirliftFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/antiship.py b/game/ato/flightplans/antiship.py index 59d4f89f..b883fd9f 100644 --- a/game/ato/flightplans/antiship.py +++ b/game/ato/flightplans/antiship.py @@ -41,5 +41,5 @@ class Builder(FormationAttackBuilder[AntiShipFlightPlan, FormationAttackLayout]) def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> list[StrikeTarget]: return [StrikeTarget(f"{g.group_name} at {tgo.name}", g) for g in tgo.groups] - def build(self) -> AntiShipFlightPlan: + def build(self, dump_debug_info: bool = False) -> AntiShipFlightPlan: return AntiShipFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/bai.py b/game/ato/flightplans/bai.py index e64e27ac..22b8f915 100644 --- a/game/ato/flightplans/bai.py +++ b/game/ato/flightplans/bai.py @@ -39,5 +39,5 @@ class Builder(FormationAttackBuilder[BaiFlightPlan, FormationAttackLayout]): return self._build(FlightWaypointType.INGRESS_BAI, targets) - def build(self) -> BaiFlightPlan: + def build(self, dump_debug_info: bool = False) -> BaiFlightPlan: return BaiFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/barcap.py b/game/ato/flightplans/barcap.py index 615dcf48..43c5535e 100644 --- a/game/ato/flightplans/barcap.py +++ b/game/ato/flightplans/barcap.py @@ -66,5 +66,5 @@ class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]): bullseye=builder.bullseye(), ) - def build(self) -> BarCapFlightPlan: + def build(self, dump_debug_info: bool = False) -> BarCapFlightPlan: return BarCapFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/cas.py b/game/ato/flightplans/cas.py index 75b367f4..5a379ced 100644 --- a/game/ato/flightplans/cas.py +++ b/game/ato/flightplans/cas.py @@ -6,13 +6,15 @@ from datetime import timedelta from typing import TYPE_CHECKING, Type from game.theater import FrontLine -from game.utils import Distance, Speed, kph, meters +from game.utils import Distance, Speed, kph, meters, dcs_to_shapely_point from .ibuilder import IBuilder from .invalidobjectivelocation import InvalidObjectiveLocation from .patrolling import PatrollingFlightPlan, PatrollingLayout from .uizonedisplay import UiZone, UiZoneDisplay from .waypointbuilder import WaypointBuilder from ..flightwaypointtype import FlightWaypointType +from ...flightplan.ipsolver import IpSolver +from ...persistence.paths import waypoint_debug_directory if TYPE_CHECKING: from ..flightwaypoint import FlightWaypoint @@ -20,13 +22,13 @@ if TYPE_CHECKING: @dataclass(frozen=True) class CasLayout(PatrollingLayout): - target: FlightWaypoint + ingress: FlightWaypoint def iter_waypoints(self) -> Iterator[FlightWaypoint]: yield self.departure yield from self.nav_to + yield self.ingress yield self.patrol_start - yield self.target yield self.patrol_end yield from self.nav_from yield self.arrival @@ -59,7 +61,7 @@ class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay): @property def combat_speed_waypoints(self) -> set[FlightWaypoint]: - return {self.layout.patrol_start, self.layout.target, self.layout.patrol_end} + return {self.layout.ingress, self.layout.patrol_start, self.layout.patrol_end} def request_escort_at(self) -> FlightWaypoint | None: return self.layout.patrol_start @@ -68,14 +70,17 @@ class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay): return self.layout.patrol_end def ui_zone(self) -> UiZone: + midpoint = ( + self.layout.patrol_start.position + self.layout.patrol_end.position + ) / 2 return UiZone( - [self.layout.target.position], + [midpoint], self.engagement_distance, ) class Builder(IBuilder[CasFlightPlan, CasLayout]): - def layout(self) -> CasLayout: + def layout(self, dump_debug_info: bool) -> CasLayout: location = self.package.target if not isinstance(location, FrontLine): @@ -86,46 +91,77 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]): ) bounds = FrontLineConflictDescription.frontline_bounds(location, self.theater) - ingress = bounds.left_position - center = bounds.center - egress = bounds.right_position + patrol_start = bounds.left_position + patrol_end = bounds.right_position - ingress_distance = ingress.distance_to_point(self.flight.departure.position) - egress_distance = egress.distance_to_point(self.flight.departure.position) - if egress_distance < ingress_distance: - ingress, egress = egress, ingress + start_distance = patrol_start.distance_to_point(self.flight.departure.position) + end_distance = patrol_end.distance_to_point(self.flight.departure.position) + if end_distance < start_distance: + patrol_start, patrol_end = patrol_end, patrol_start builder = WaypointBuilder(self.flight, self.coalition) is_helo = self.flight.unit_type.dcs_unit_type.helicopter - ingress_egress_altitude = ( - self.doctrine.ingress_altitude if not is_helo else meters(50) + patrol_altitude = self.doctrine.ingress_altitude if not is_helo else meters(50) + use_agl_patrol_altitude = is_helo + + ip_solver = IpSolver( + dcs_to_shapely_point(self.flight.departure.position), + dcs_to_shapely_point(patrol_start), + self.doctrine, + self.threat_zones.all, ) - use_agl_ingress_egress = is_helo + ip_solver.set_debug_properties( + waypoint_debug_directory() / "IP", self.theater.terrain + ) + ingress_point_shapely = ip_solver.solve() + if dump_debug_info: + ip_solver.dump_debug_info() + + ingress_point = patrol_start.new_in_same_map( + ingress_point_shapely.x, ingress_point_shapely.y + ) + + patrol_start_waypoint = builder.nav( + patrol_start, patrol_altitude, use_agl_patrol_altitude + ) + patrol_start_waypoint.name = "FLOT START" + patrol_start_waypoint.pretty_name = "FLOT start" + patrol_start_waypoint.description = "FLOT boundary" + + patrol_end_waypoint = builder.nav( + patrol_end, patrol_altitude, use_agl_patrol_altitude + ) + patrol_end_waypoint.name = "FLOT END" + patrol_end_waypoint.pretty_name = "FLOT end" + patrol_end_waypoint.description = "FLOT boundary" + + ingress = builder.ingress( + FlightWaypointType.INGRESS_CAS, ingress_point, location + ) + ingress.description = f"Ingress to provide CAS at {location}" return CasLayout( departure=builder.takeoff(self.flight.departure), nav_to=builder.nav_path( self.flight.departure.position, - ingress, - ingress_egress_altitude, - use_agl_ingress_egress, + ingress_point, + patrol_altitude, + use_agl_patrol_altitude, ), nav_from=builder.nav_path( - egress, + patrol_end, self.flight.arrival.position, - ingress_egress_altitude, - use_agl_ingress_egress, + patrol_altitude, + use_agl_patrol_altitude, ), - patrol_start=builder.ingress( - FlightWaypointType.INGRESS_CAS, ingress, location - ), - target=builder.cas(center), - patrol_end=builder.egress(egress, location), + ingress=ingress, + patrol_start=patrol_start_waypoint, + patrol_end=patrol_end_waypoint, arrival=builder.land(self.flight.arrival), divert=builder.divert(self.flight.divert), bullseye=builder.bullseye(), ) - def build(self) -> CasFlightPlan: - return CasFlightPlan(self.flight, self.layout()) + def build(self, dump_debug_info: bool = False) -> CasFlightPlan: + return CasFlightPlan(self.flight, self.layout(dump_debug_info)) diff --git a/game/ato/flightplans/custom.py b/game/ato/flightplans/custom.py index 19c0b559..abb3454f 100644 --- a/game/ato/flightplans/custom.py +++ b/game/ato/flightplans/custom.py @@ -72,5 +72,5 @@ class Builder(IBuilder[CustomFlightPlan, CustomLayout]): builder = WaypointBuilder(self.flight, self.coalition) return CustomLayout(builder.takeoff(self.flight.departure), self.waypoints) - def build(self) -> CustomFlightPlan: + def build(self, dump_debug_info: bool = False) -> CustomFlightPlan: return CustomFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/dead.py b/game/ato/flightplans/dead.py index 6827130d..7f240ced 100644 --- a/game/ato/flightplans/dead.py +++ b/game/ato/flightplans/dead.py @@ -37,5 +37,5 @@ class Builder(FormationAttackBuilder[DeadFlightPlan, FormationAttackLayout]): return self._build(FlightWaypointType.INGRESS_DEAD) - def build(self) -> DeadFlightPlan: + def build(self, dump_debug_info: bool = False) -> DeadFlightPlan: return DeadFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/escort.py b/game/ato/flightplans/escort.py index a176fd40..5e1ca54d 100644 --- a/game/ato/flightplans/escort.py +++ b/game/ato/flightplans/escort.py @@ -50,5 +50,5 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]): bullseye=builder.bullseye(), ) - def build(self) -> EscortFlightPlan: + def build(self, dump_debug_info: bool = False) -> EscortFlightPlan: return EscortFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/ferry.py b/game/ato/flightplans/ferry.py index 2a6fa3a0..e7c2f3b3 100644 --- a/game/ato/flightplans/ferry.py +++ b/game/ato/flightplans/ferry.py @@ -83,5 +83,5 @@ class Builder(IBuilder[FerryFlightPlan, FerryLayout]): bullseye=builder.bullseye(), ) - def build(self) -> FerryFlightPlan: + def build(self, dump_debug_info: bool = False) -> FerryFlightPlan: return FerryFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/ibuilder.py b/game/ato/flightplans/ibuilder.py index b85589e4..a91cd6f3 100644 --- a/game/ato/flightplans/ibuilder.py +++ b/game/ato/flightplans/ibuilder.py @@ -35,7 +35,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]): def regenerate(self, dump_debug_info: bool = False) -> None: try: self._generate_package_waypoints_if_needed(dump_debug_info) - self._flight_plan = self.build() + self._flight_plan = self.build(dump_debug_info) except NavMeshError as ex: color = "blue" if self.flight.squadron.player else "red" raise PlanningError( @@ -59,11 +59,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]): return self.flight.departure.theater @abstractmethod - def layout(self) -> LayoutT: - ... - - @abstractmethod - def build(self) -> FlightPlanT: + def build(self, dump_debug_info: bool = False) -> FlightPlanT: ... @property diff --git a/game/ato/flightplans/ocaaircraft.py b/game/ato/flightplans/ocaaircraft.py index 5b8ba6da..61cffce1 100644 --- a/game/ato/flightplans/ocaaircraft.py +++ b/game/ato/flightplans/ocaaircraft.py @@ -32,5 +32,5 @@ class Builder(FormationAttackBuilder[OcaAircraftFlightPlan, FormationAttackLayou return self._build(FlightWaypointType.INGRESS_OCA_AIRCRAFT) - def build(self) -> OcaAircraftFlightPlan: + def build(self, dump_debug_info: bool = False) -> OcaAircraftFlightPlan: return OcaAircraftFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/ocarunway.py b/game/ato/flightplans/ocarunway.py index fd7b3bfd..234d8f41 100644 --- a/game/ato/flightplans/ocarunway.py +++ b/game/ato/flightplans/ocarunway.py @@ -32,5 +32,5 @@ class Builder(FormationAttackBuilder[OcaRunwayFlightPlan, FormationAttackLayout] return self._build(FlightWaypointType.INGRESS_OCA_RUNWAY) - def build(self) -> OcaRunwayFlightPlan: + def build(self, dump_debug_info: bool = False) -> OcaRunwayFlightPlan: return OcaRunwayFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/packagerefueling.py b/game/ato/flightplans/packagerefueling.py index cec75cd8..0e0b4d86 100644 --- a/game/ato/flightplans/packagerefueling.py +++ b/game/ato/flightplans/packagerefueling.py @@ -121,5 +121,5 @@ class Builder(IBuilder[PackageRefuelingFlightPlan, PatrollingLayout]): bullseye=builder.bullseye(), ) - def build(self) -> PackageRefuelingFlightPlan: + def build(self, dump_debug_info: bool = False) -> PackageRefuelingFlightPlan: return PackageRefuelingFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/rtb.py b/game/ato/flightplans/rtb.py index 56e87268..34fb0206 100644 --- a/game/ato/flightplans/rtb.py +++ b/game/ato/flightplans/rtb.py @@ -93,5 +93,5 @@ class Builder(IBuilder[RtbFlightPlan, RtbLayout]): bullseye=builder.bullseye(), ) - def build(self) -> RtbFlightPlan: + def build(self, dump_debug_info: bool = False) -> RtbFlightPlan: return RtbFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/sead.py b/game/ato/flightplans/sead.py index 3c6e89ae..2a691026 100644 --- a/game/ato/flightplans/sead.py +++ b/game/ato/flightplans/sead.py @@ -24,5 +24,5 @@ class Builder(FormationAttackBuilder[SeadFlightPlan, FormationAttackLayout]): def layout(self) -> FormationAttackLayout: return self._build(FlightWaypointType.INGRESS_SEAD) - def build(self) -> SeadFlightPlan: + def build(self, dump_debug_info: bool = False) -> SeadFlightPlan: return SeadFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/shiprecoverytanker.py b/game/ato/flightplans/shiprecoverytanker.py index cc758eff..22746989 100644 --- a/game/ato/flightplans/shiprecoverytanker.py +++ b/game/ato/flightplans/shiprecoverytanker.py @@ -91,5 +91,5 @@ class Builder(IBuilder[RecoveryTankerFlightPlan, RecoveryTankerLayout]): bullseye=builder.bullseye(), ) - def build(self) -> RecoveryTankerFlightPlan: + def build(self, dump_debug_info: bool = False) -> RecoveryTankerFlightPlan: return RecoveryTankerFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/strike.py b/game/ato/flightplans/strike.py index 303c50b6..e04387c3 100644 --- a/game/ato/flightplans/strike.py +++ b/game/ato/flightplans/strike.py @@ -32,5 +32,5 @@ class Builder(FormationAttackBuilder[StrikeFlightPlan, FormationAttackLayout]): return self._build(FlightWaypointType.INGRESS_STRIKE, targets) - def build(self) -> StrikeFlightPlan: + def build(self, dump_debug_info: bool = False) -> StrikeFlightPlan: return StrikeFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/sweep.py b/game/ato/flightplans/sweep.py index 2161e8d5..aba95c2d 100644 --- a/game/ato/flightplans/sweep.py +++ b/game/ato/flightplans/sweep.py @@ -137,5 +137,5 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]): target, origin, ip, join, self.coalition, self.theater ).find_best_hold_point() - def build(self) -> SweepFlightPlan: + def build(self, dump_debug_info: bool = False) -> SweepFlightPlan: return SweepFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/tarcap.py b/game/ato/flightplans/tarcap.py index 7542dffe..3f6d4ac4 100644 --- a/game/ato/flightplans/tarcap.py +++ b/game/ato/flightplans/tarcap.py @@ -122,5 +122,5 @@ class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]): bullseye=builder.bullseye(), ) - def build(self) -> TarCapFlightPlan: + def build(self, dump_debug_info: bool = False) -> TarCapFlightPlan: return TarCapFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/theaterrefueling.py b/game/ato/flightplans/theaterrefueling.py index 08ed94eb..d93eb4a8 100644 --- a/game/ato/flightplans/theaterrefueling.py +++ b/game/ato/flightplans/theaterrefueling.py @@ -79,5 +79,5 @@ class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]): bullseye=builder.bullseye(), ) - def build(self) -> TheaterRefuelingFlightPlan: + def build(self, dump_debug_info: bool = False) -> TheaterRefuelingFlightPlan: return TheaterRefuelingFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/waypointbuilder.py b/game/ato/flightplans/waypointbuilder.py index e9cd233e..8cb75cdd 100644 --- a/game/ato/flightplans/waypointbuilder.py +++ b/game/ato/flightplans/waypointbuilder.py @@ -253,21 +253,6 @@ class WaypointBuilder: targets=objective.strike_targets, ) - def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint: - alt_type: AltitudeReference = "BARO" - if self.is_helo: - alt_type = "RADIO" - - return FlightWaypoint( - "EGRESS", - FlightWaypointType.EGRESS, - position, - meters(60) if self.is_helo else self.doctrine.ingress_altitude, - alt_type, - description=f"EGRESS from {target.name}", - pretty_name=f"EGRESS from {target.name}", - ) - def bai_group(self, target: StrikeTarget) -> FlightWaypoint: return self._target_point(target, f"ATTACK {target.name}") @@ -357,17 +342,6 @@ class WaypointBuilder: waypoint.only_for_player = True return waypoint - def cas(self, position: Point) -> FlightWaypoint: - return FlightWaypoint( - "CAS", - FlightWaypointType.CAS, - position, - meters(60) if self.is_helo else meters(1000), - "RADIO", - description="Provide CAS", - pretty_name="CAS", - ) - @staticmethod def race_track_start(position: Point, altitude: Distance) -> FlightWaypoint: """Creates a racetrack start waypoint. diff --git a/game/ato/flightwaypointtype.py b/game/ato/flightwaypointtype.py index 5c489746..ab79fd5c 100644 --- a/game/ato/flightwaypointtype.py +++ b/game/ato/flightwaypointtype.py @@ -25,7 +25,7 @@ class FlightWaypointType(IntEnum): INGRESS_STRIKE = 5 # Ingress strike (For generator, means that this should have bombing on next TARGET_POINT points) INGRESS_SEAD = 6 # Ingress sead (For generator, means that this should attack groups on TARGET_GROUP_LOC points) INGRESS_CAS = 7 # Ingress cas (should start CAS task) - CAS = 8 # Should do CAS there + CAS = 8 # Unused. EGRESS = 9 # Should stop attack DESCENT_POINT = 10 # Should start descending to pattern alt LANDING_POINT = 11 # Should land there diff --git a/game/ato/packagewaypoints.py b/game/ato/packagewaypoints.py index d53bc558..c0450576 100644 --- a/game/ato/packagewaypoints.py +++ b/game/ato/packagewaypoints.py @@ -9,7 +9,7 @@ from game.ato.flightplans.waypointbuilder import WaypointBuilder from game.flightplan import JoinZoneGeometry from game.flightplan.ipsolver import IpSolver from game.flightplan.refuelzonegeometry import RefuelZoneGeometry -from game.persistence.paths import liberation_user_dir +from game.persistence.paths import waypoint_debug_directory from game.utils import dcs_to_shapely_point if TYPE_CHECKING: @@ -30,8 +30,6 @@ class PackageWaypoints: ) -> PackageWaypoints: origin = package.departure_closest_to_target() - waypoint_debug_directory = liberation_user_dir() / "Debug/Waypoints" - # Start by picking the best IP for the attack. ip_solver = IpSolver( dcs_to_shapely_point(origin.position), @@ -40,7 +38,7 @@ class PackageWaypoints: coalition.opponent.threat_zone.all, ) ip_solver.set_debug_properties( - waypoint_debug_directory / "IP", coalition.game.theater.terrain + waypoint_debug_directory() / "IP", coalition.game.theater.terrain ) ingress_point_shapely = ip_solver.solve() if dump_debug_info: diff --git a/game/missiongenerator/aircraft/waypoints/casingress.py b/game/missiongenerator/aircraft/waypoints/casingress.py index d63b20dc..fc39b8c8 100644 --- a/game/missiongenerator/aircraft/waypoints/casingress.py +++ b/game/missiongenerator/aircraft/waypoints/casingress.py @@ -11,9 +11,13 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder class CasIngressBuilder(PydcsWaypointBuilder): def add_tasks(self, waypoint: MovingPoint) -> None: if isinstance(self.flight.flight_plan, CasFlightPlan): + patrol_center = ( + self.flight.flight_plan.layout.patrol_start.position + + self.flight.flight_plan.layout.patrol_end.position + ) / 2 waypoint.add_task( EngageTargetsInZone( - position=self.flight.flight_plan.layout.target.position, + position=patrol_center, radius=int(self.flight.flight_plan.engagement_distance.meters), targets=[ Targets.All.GroundUnits.GroundVehicles, diff --git a/game/persistence/paths.py b/game/persistence/paths.py index d32cfb44..6133e457 100644 --- a/game/persistence/paths.py +++ b/game/persistence/paths.py @@ -29,3 +29,7 @@ def save_dir() -> Path: def mission_path_for(name: str) -> Path: return Path(base_path()) / "Missions" / name + + +def waypoint_debug_directory() -> Path: + return liberation_user_dir() / "Debug/Waypoints"