mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Ability to plan tanker recovery for strike like flights (#1729)
* Initial refueling definitions. * Adding refuel definitions. * Initial functionality changes * Regenerate package when adding refueling flight. * Recursively change package waypoint. * Fix mypy errors. * Regenerate flight plans when tanker is added to package. * Give tanker better starting position on package recovery. * Add TOT calculation for refueling waypoint. * Timing changes to Strike split point and Refueling start time. * Add correct waypoint builder for refuel in tarcap and sweep. Remove restrict afterburner on refuel point. * Always generate a refuel point for a package. * Less arbitrary altitude in Refuel track start time calculation. * Refueling waypoint no longer optional. * Fix mypy gen error. * Better discrimination of which tanker flight plan to make. * Remove refuel tot calculations. * Remove package regeneration on tanker flight addition.
This commit is contained in:
parent
94f65d8f70
commit
532ac261ff
@ -5,6 +5,7 @@ Saves from 5.x are not compatible with 6.0.
|
||||
## Features/Improvements
|
||||
|
||||
* **[Mission Generation]** Added an option to fast-forward mission generation until the point of first contact (WIP).
|
||||
* **[Flight Planning]** Added the ability to plan tankers for recovery on package flights. AI does not plan.
|
||||
|
||||
## Fixes
|
||||
|
||||
|
||||
@ -45,3 +45,4 @@ class FlightWaypointType(Enum):
|
||||
PICKUP = 26
|
||||
DROP_OFF = 27
|
||||
BULLSEYE = 28
|
||||
REFUEL = 29 # Should look for nearby tanker to refuel from.
|
||||
|
||||
@ -48,7 +48,7 @@ class Package:
|
||||
"""The speed of the package when in formation.
|
||||
|
||||
If none of the flights in the package will join a formation, this
|
||||
returns None. This is nto uncommon, since only strike-like (strike,
|
||||
returns None. This is not uncommon, since only strike-like (strike,
|
||||
DEAD, anti-ship, BAI, etc.) flights and their escorts fly in formation.
|
||||
Others (CAP and CAS, currently) will coordinate in target timing but
|
||||
fly their own path to the target.
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from dcs import Point
|
||||
|
||||
@ -8,3 +9,4 @@ class PackageWaypoints:
|
||||
join: Point
|
||||
ingress: Point
|
||||
split: Point
|
||||
refuel: Point
|
||||
|
||||
27
game/flightplan/refuelzonegeometry.py
Normal file
27
game/flightplan/refuelzonegeometry.py
Normal file
@ -0,0 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dcs import Point
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
|
||||
|
||||
class RefuelZoneGeometry:
|
||||
def __init__(
|
||||
self,
|
||||
package_home: Point,
|
||||
join: Point,
|
||||
coalition: Coalition,
|
||||
) -> None:
|
||||
self.package_home = package_home
|
||||
self.join = join
|
||||
self.coalition = coalition
|
||||
|
||||
def find_best_refuel_point(self) -> Point:
|
||||
# Do simple at first.
|
||||
# TODO: Consider threats.
|
||||
distance = 0.75 * self.package_home.distance_to_point(self.join)
|
||||
heading = self.package_home.heading_between_point(self.join)
|
||||
return self.package_home.point_from_heading(heading, distance)
|
||||
9
game/missiongenerator/aircraft/waypoints/refuel.py
Normal file
9
game/missiongenerator/aircraft/waypoints/refuel.py
Normal file
@ -0,0 +1,9 @@
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import RefuelingTaskAction
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class RefuelPointBuilder(PydcsWaypointBuilder):
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
waypoint.add_task(RefuelingTaskAction())
|
||||
return super().add_tasks(waypoint)
|
||||
@ -32,6 +32,7 @@ from .ocarunwayingress import OcaRunwayIngressBuilder
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder, TARGET_WAYPOINTS
|
||||
from .racetrack import RaceTrackBuilder
|
||||
from .racetrackend import RaceTrackEndBuilder
|
||||
from .refuel import RefuelPointBuilder
|
||||
from .seadingress import SeadIngressBuilder
|
||||
from .strikeingress import StrikeIngressBuilder
|
||||
from .sweepingress import SweepIngressBuilder
|
||||
@ -130,6 +131,7 @@ class WaypointGenerator:
|
||||
FlightWaypointType.PATROL: RaceTrackEndBuilder,
|
||||
FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
|
||||
FlightWaypointType.PICKUP: CargoStopBuilder,
|
||||
FlightWaypointType.REFUEL: RefuelPointBuilder,
|
||||
}
|
||||
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
|
||||
return builder(
|
||||
|
||||
@ -906,11 +906,12 @@ class Airfield(ControlPoint):
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
FlightType.AEWC,
|
||||
FlightType.REFUELING,
|
||||
# TODO: FlightType.INTERCEPTION
|
||||
# TODO: FlightType.LOGISTICS
|
||||
]
|
||||
|
||||
yield FlightType.REFUELING
|
||||
|
||||
@property
|
||||
def total_aircraft_parking(self) -> int:
|
||||
"""
|
||||
|
||||
@ -132,6 +132,7 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
|
||||
yield from [
|
||||
FlightType.STRIKE,
|
||||
FlightType.BAI,
|
||||
FlightType.REFUELING,
|
||||
]
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ from game.ato.starttype import StartType
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.dcs.aircrafttype import FuelConsumption
|
||||
from game.flightplan import HoldZoneGeometry, IpZoneGeometry, JoinZoneGeometry
|
||||
from game.flightplan.refuelzonegeometry import RefuelZoneGeometry
|
||||
from game.theater import (
|
||||
Airfield,
|
||||
ConflictTheater,
|
||||
@ -51,7 +52,6 @@ if TYPE_CHECKING:
|
||||
from game.ato.package import Package
|
||||
from game.coalition import Coalition
|
||||
from game.threatzones import ThreatZones
|
||||
from game.transfers import Convoy
|
||||
|
||||
|
||||
INGRESS_TYPES = {
|
||||
@ -362,6 +362,7 @@ class LoiterFlightPlan(FlightPlan):
|
||||
class FormationFlightPlan(LoiterFlightPlan):
|
||||
join: FlightWaypoint
|
||||
split: FlightWaypoint
|
||||
refuel: FlightWaypoint
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
raise NotImplementedError
|
||||
@ -555,6 +556,7 @@ class CasFlightPlan(PatrollingFlightPlan):
|
||||
@dataclass(frozen=True)
|
||||
class TarCapFlightPlan(PatrollingFlightPlan):
|
||||
takeoff: FlightWaypoint
|
||||
refuel: Optional[FlightWaypoint]
|
||||
land: FlightWaypoint
|
||||
divert: Optional[FlightWaypoint]
|
||||
bullseye: FlightWaypoint
|
||||
@ -567,6 +569,8 @@ class TarCapFlightPlan(PatrollingFlightPlan):
|
||||
self.patrol_start,
|
||||
self.patrol_end,
|
||||
]
|
||||
if self.refuel is not None:
|
||||
yield self.refuel
|
||||
yield from self.nav_from
|
||||
yield self.land
|
||||
if self.divert is not None:
|
||||
@ -624,6 +628,8 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
yield self.ingress
|
||||
yield from self.targets
|
||||
yield self.split
|
||||
if self.refuel is not None:
|
||||
yield self.refuel
|
||||
yield from self.nav_from
|
||||
yield self.land
|
||||
if self.divert is not None:
|
||||
@ -697,8 +703,20 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
|
||||
@property
|
||||
def split_time(self) -> timedelta:
|
||||
travel_time = self.travel_time_between_waypoints(self.ingress, self.split)
|
||||
return self.ingress_time + travel_time
|
||||
travel_time_ingress = self.travel_time_between_waypoints(
|
||||
self.ingress, self.target_area_waypoint
|
||||
)
|
||||
travel_time_egress = self.travel_time_between_waypoints(
|
||||
self.target_area_waypoint, self.split
|
||||
)
|
||||
minutes_at_target = 0.75 * len(self.targets)
|
||||
timedelta_at_target = timedelta(minutes=minutes_at_target)
|
||||
return (
|
||||
self.ingress_time
|
||||
+ travel_time_ingress
|
||||
+ timedelta_at_target
|
||||
+ travel_time_egress
|
||||
)
|
||||
|
||||
@property
|
||||
def ingress_time(self) -> timedelta:
|
||||
@ -722,6 +740,7 @@ class SweepFlightPlan(LoiterFlightPlan):
|
||||
nav_to: List[FlightWaypoint]
|
||||
sweep_start: FlightWaypoint
|
||||
sweep_end: FlightWaypoint
|
||||
refuel: FlightWaypoint
|
||||
nav_from: List[FlightWaypoint]
|
||||
land: FlightWaypoint
|
||||
divert: Optional[FlightWaypoint]
|
||||
@ -734,6 +753,8 @@ class SweepFlightPlan(LoiterFlightPlan):
|
||||
yield from self.nav_to
|
||||
yield self.sweep_start
|
||||
yield self.sweep_end
|
||||
if self.refuel is not None:
|
||||
yield self.refuel
|
||||
yield from self.nav_from
|
||||
yield self.land
|
||||
if self.divert is not None:
|
||||
@ -835,6 +856,10 @@ class RefuelingFlightPlan(PatrollingFlightPlan):
|
||||
divert: Optional[FlightWaypoint]
|
||||
bullseye: FlightWaypoint
|
||||
|
||||
@property
|
||||
def patrol_start_time(self) -> timedelta:
|
||||
return self.package.time_over_target
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.takeoff
|
||||
yield from self.nav_to
|
||||
@ -847,6 +872,52 @@ class RefuelingFlightPlan(PatrollingFlightPlan):
|
||||
yield self.bullseye
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PackageRefuelingFlightPlan(RefuelingFlightPlan):
|
||||
def target_area_waypoint(self) -> FlightWaypoint:
|
||||
return FlightWaypoint(
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
self.package.target.position.x,
|
||||
self.package.target.position.y,
|
||||
meters(0),
|
||||
)
|
||||
|
||||
@property
|
||||
def patrol_start_time(self) -> timedelta:
|
||||
altitude: Optional[Distance] = self.flight.unit_type.patrol_altitude
|
||||
|
||||
if altitude is None:
|
||||
altitude = Distance.from_feet(20000)
|
||||
|
||||
assert self.package.waypoints is not None
|
||||
|
||||
# Cheat in a FlightWaypoint for the split point.
|
||||
split: Point = self.package.waypoints.split
|
||||
split_waypoint: FlightWaypoint = FlightWaypoint(
|
||||
FlightWaypointType.SPLIT, split.x, split.y, altitude
|
||||
)
|
||||
|
||||
# Cheat in a FlightWaypoint for the refuel point.
|
||||
refuel: Point = self.package.waypoints.refuel
|
||||
refuel_waypoint: FlightWaypoint = FlightWaypoint(
|
||||
FlightWaypointType.REFUEL, refuel.x, refuel.y, altitude
|
||||
)
|
||||
|
||||
delay_target_to_split: timedelta = self.travel_time_between_waypoints(
|
||||
self.target_area_waypoint(), split_waypoint
|
||||
)
|
||||
delay_split_to_refuel: timedelta = self.travel_time_between_waypoints(
|
||||
split_waypoint, refuel_waypoint
|
||||
)
|
||||
|
||||
return (
|
||||
self.package.time_over_target
|
||||
+ delay_target_to_split
|
||||
+ delay_split_to_refuel
|
||||
- timedelta(minutes=1.5)
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AirliftFlightPlan(FlightPlan):
|
||||
takeoff: FlightWaypoint
|
||||
@ -1043,11 +1114,24 @@ class FlightPlanBuilder:
|
||||
elif task == FlightType.TRANSPORT:
|
||||
return self.generate_transport(flight)
|
||||
elif task == FlightType.REFUELING:
|
||||
if self.package.target.is_friendly(self.is_player) or isinstance(
|
||||
self.package.target, FrontLine
|
||||
):
|
||||
return self.generate_refueling_racetrack(flight)
|
||||
else:
|
||||
return self.generate_refueling_package_support(flight)
|
||||
elif task == FlightType.FERRY:
|
||||
return self.generate_ferry(flight)
|
||||
raise PlanningError(f"{task} flight plan generation not implemented")
|
||||
|
||||
def regenerate_flight_plans(self) -> None:
|
||||
new_flights: list[Flight] = []
|
||||
for old_flight in self.package.flights:
|
||||
# TODO: Don't lose custom targets here.
|
||||
old_flight.flight_plan = self.generate_flight_plan(old_flight, None)
|
||||
new_flights.append(old_flight)
|
||||
self.package.flights = new_flights
|
||||
|
||||
def regenerate_package_waypoints(self) -> None:
|
||||
from game.ato.packagewaypoints import PackageWaypoints
|
||||
|
||||
@ -1067,6 +1151,12 @@ class FlightPlanBuilder:
|
||||
self.coalition,
|
||||
).find_best_join_point()
|
||||
|
||||
refuel_point = RefuelZoneGeometry(
|
||||
package_airfield.position,
|
||||
join_point,
|
||||
self.coalition,
|
||||
).find_best_refuel_point()
|
||||
|
||||
# And the split point based on the best route from the IP. Since that's no
|
||||
# different than the best route *to* the IP, this is the same as the join point.
|
||||
# TODO: Estimate attack completion point based on the IP and split from there?
|
||||
@ -1074,6 +1164,7 @@ class FlightPlanBuilder:
|
||||
WaypointBuilder.perturb(join_point),
|
||||
ingress_point,
|
||||
WaypointBuilder.perturb(join_point),
|
||||
refuel_point,
|
||||
)
|
||||
|
||||
def generate_strike(self, flight: Flight) -> StrikeFlightPlan:
|
||||
@ -1241,7 +1332,7 @@ class FlightPlanBuilder:
|
||||
)
|
||||
|
||||
def generate_sweep(self, flight: Flight) -> SweepFlightPlan:
|
||||
"""Generate a BARCAP flight at a given location.
|
||||
"""Generate a FighterSweep flight at a given location.
|
||||
|
||||
Args:
|
||||
flight: The flight to generate the flight plan for.
|
||||
@ -1260,6 +1351,11 @@ class FlightPlanBuilder:
|
||||
|
||||
hold = builder.hold(self._hold_point(flight))
|
||||
|
||||
refuel = None
|
||||
|
||||
if self.package.waypoints is not None:
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
|
||||
return SweepFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
@ -1275,6 +1371,7 @@ class FlightPlanBuilder:
|
||||
),
|
||||
sweep_start=start,
|
||||
sweep_end=end,
|
||||
refuel=refuel,
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
@ -1482,6 +1579,12 @@ class FlightPlanBuilder:
|
||||
orbit0p, orbit1p = self.cap_racetrack_for_objective(location, barcap=False)
|
||||
|
||||
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
|
||||
|
||||
refuel = None
|
||||
|
||||
if self.package.waypoints is not None:
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
|
||||
return TarCapFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
@ -1498,6 +1601,7 @@ class FlightPlanBuilder:
|
||||
nav_from=builder.nav_path(orbit1p, flight.arrival.position, patrol_alt),
|
||||
patrol_start=start,
|
||||
patrol_end=end,
|
||||
refuel=refuel,
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
@ -1611,6 +1715,9 @@ class FlightPlanBuilder:
|
||||
hold = builder.hold(self._hold_point(flight))
|
||||
join = builder.join(self.package.waypoints.join)
|
||||
split = builder.split(self.package.waypoints.split)
|
||||
refuel = None
|
||||
if self.package.waypoints.refuel is not None:
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
|
||||
return StrikeFlightPlan(
|
||||
package=self.package,
|
||||
@ -1625,6 +1732,7 @@ class FlightPlanBuilder:
|
||||
ingress=ingress,
|
||||
targets=[target],
|
||||
split=split,
|
||||
refuel=refuel,
|
||||
nav_from=builder.nav_path(
|
||||
split.position, flight.arrival.position, self.doctrine.ingress_altitude
|
||||
),
|
||||
@ -1703,6 +1811,11 @@ class FlightPlanBuilder:
|
||||
)
|
||||
|
||||
def generate_refueling_racetrack(self, flight: Flight) -> RefuelingFlightPlan:
|
||||
|
||||
racetrack_half_distance = Distance.from_nautical_miles(20).meters
|
||||
|
||||
patrol_duration = timedelta(hours=1)
|
||||
|
||||
location = self.package.target
|
||||
|
||||
closest_boundary = self.threat_zones.closest_boundary(location.position)
|
||||
@ -1725,11 +1838,10 @@ class FlightPlanBuilder:
|
||||
orbit_heading.degrees, orbit_distance.meters
|
||||
)
|
||||
|
||||
racetrack_half_distance = Distance.from_nautical_miles(20).meters
|
||||
|
||||
racetrack_start = racetrack_center.point_from_heading(
|
||||
orbit_heading.right.degrees, racetrack_half_distance
|
||||
)
|
||||
|
||||
racetrack_end = racetrack_center.point_from_heading(
|
||||
orbit_heading.left.degrees, racetrack_half_distance
|
||||
)
|
||||
@ -1764,7 +1876,74 @@ class FlightPlanBuilder:
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
patrol_duration=timedelta(hours=1),
|
||||
patrol_duration=patrol_duration,
|
||||
patrol_speed=speed,
|
||||
# TODO: Factor out a common base of the combat and non-combat race-tracks.
|
||||
# No harm in setting this, but we ought to clean up a bit.
|
||||
engagement_distance=meters(0),
|
||||
)
|
||||
|
||||
def generate_refueling_package_support(
|
||||
self, flight: Flight
|
||||
) -> PackageRefuelingFlightPlan:
|
||||
package_waypoints = self.package.waypoints
|
||||
assert package_waypoints is not None
|
||||
|
||||
racetrack_half_distance = Distance.from_nautical_miles(20).meters
|
||||
# TODO: Only consider aircraft that can refuel with this tanker type.
|
||||
refuel_time_minutes = 5
|
||||
for flight in self.package.flights:
|
||||
flight_size = flight.roster.max_size
|
||||
refuel_time_minutes = refuel_time_minutes + 4 * flight_size + 1
|
||||
|
||||
patrol_duration = timedelta(minutes=refuel_time_minutes)
|
||||
|
||||
racetrack_center = package_waypoints.refuel
|
||||
|
||||
split_heading = Heading.from_degrees(
|
||||
racetrack_center.heading_between_point(package_waypoints.split)
|
||||
)
|
||||
home_heading = split_heading.opposite
|
||||
|
||||
racetrack_start = racetrack_center.point_from_heading(
|
||||
split_heading.degrees, racetrack_half_distance
|
||||
)
|
||||
|
||||
racetrack_end = racetrack_center.point_from_heading(
|
||||
home_heading.degrees, racetrack_half_distance
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(flight, self.coalition)
|
||||
|
||||
tanker_type = flight.unit_type
|
||||
if tanker_type.patrol_altitude is not None:
|
||||
altitude = tanker_type.patrol_altitude
|
||||
else:
|
||||
altitude = feet(21000)
|
||||
|
||||
# TODO: Could use flight.unit_type.preferred_patrol_speed(altitude) instead.
|
||||
if tanker_type.patrol_speed is not None:
|
||||
speed = tanker_type.patrol_speed
|
||||
else:
|
||||
# ~280 knots IAS at 21000.
|
||||
speed = knots(400)
|
||||
|
||||
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
|
||||
|
||||
return PackageRefuelingFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
flight.departure.position, racetrack_start, altitude
|
||||
),
|
||||
nav_from=builder.nav_path(racetrack_end, flight.arrival.position, altitude),
|
||||
patrol_start=racetrack[0],
|
||||
patrol_end=racetrack[1],
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
patrol_duration=patrol_duration,
|
||||
patrol_speed=speed,
|
||||
# TODO: Factor out a common base of the combat and non-combat race-tracks.
|
||||
# No harm in setting this, but we ought to clean up a bit.
|
||||
@ -1843,6 +2022,9 @@ class FlightPlanBuilder:
|
||||
hold = builder.hold(self._hold_point(flight))
|
||||
join = builder.join(self.package.waypoints.join)
|
||||
split = builder.split(self.package.waypoints.split)
|
||||
refuel = None
|
||||
if self.package.waypoints.refuel is not None:
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
|
||||
return StrikeFlightPlan(
|
||||
package=self.package,
|
||||
@ -1859,6 +2041,7 @@ class FlightPlanBuilder:
|
||||
),
|
||||
targets=target_waypoints,
|
||||
split=split,
|
||||
refuel=refuel,
|
||||
nav_from=builder.nav_path(
|
||||
split.position, flight.arrival.position, self.doctrine.ingress_altitude
|
||||
),
|
||||
|
||||
@ -199,6 +199,20 @@ class WaypointBuilder:
|
||||
waypoint.name = "JOIN"
|
||||
return waypoint
|
||||
|
||||
def refuel(self, position: Point) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.REFUEL,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
||||
)
|
||||
if self.is_helo:
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.pretty_name = "Refuel"
|
||||
waypoint.description = "Refuel from tanker"
|
||||
waypoint.name = "REFUEL"
|
||||
return waypoint
|
||||
|
||||
def split(self, position: Point) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.SPLIT,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user