Compare commits

...

7 Commits

Author SHA1 Message Date
Raffson
801e9efe8c Fix heli spawn/landing at FOB/FARP
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2719.

(cherry picked from commit 34de855b8f889b72406c101d0cea385988e24bf9)
(cherry picked from commit b41ef0ab13)
2023-02-05 14:41:47 -08:00
Dan Albert
5708fe625b Don't generate runway data for heliports.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2710.

(cherry picked from commit c33a0d5deb)
2023-02-05 14:41:47 -08:00
Dan Albert
1dda0c9497 Pin version of black used in GHA.
Black rolls out style changes every year, and using "stable" means that
the check run on PRs might start formatting differently than we do
locally, or require a reformat of the codebase to make a PR submittable.

Pin to the version that we've been using. We should update to 23 at some
point, but we want to do that deliberately.

(cherry picked from commit 937bacacb7)
2023-02-05 12:15:50 -08:00
Dan Albert
56d7641251 Fix line endings.
(cherry-picked from commit e8824e5d03)
2023-02-05 12:15:50 -08:00
Dan Albert
938d6b4bdf Fix invalid tanker planning.
All three refueling missions share a common task type and differentiate
their behavior based on the type and allegiance of the package target.
This means that if the theater commander identifies a carrier as the
best location for a theater refueling task, a recovery tanker will be
planned by mistake.

If this happens on a sunken carrier, mission generation will fail
because a recovery tanker cannot be generated for a sunken carrier.

Fix the crash and the misplanned theater tanker by preventing the
commander from choosing fleet control points as refueling targets.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2693.

(cherry picked from commit eea98b01f6)
2023-01-28 14:05:57 -08:00
Dan Albert
5caf07f476 Fix unit ID of the KS-19.
It seems like this unit has never worked because of the unit ID
mismatch.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2701.

(cherry picked from commit 0df268f331)
2023-01-28 12:10:38 -08:00
Dan Albert
d1d4fde6a3 Update game version to 6.1.1. 2023-01-28 12:10:38 -08:00
14 changed files with 483 additions and 447 deletions

View File

@@ -11,6 +11,7 @@ jobs:
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2
- uses: psf/black@stable - uses: psf/black@stable
with: with:
version: ~=22.12
src: "." src: "."
options: "--check" options: "--check"

View File

@@ -1,3 +1,12 @@
# 6.1.1
## Fixes
* **[Data]** Fixed unit ID for the KS-19 AAA. KS-19 would not previously generate correctly in missions. A new game is required for this fix to take effect.
* **[Flight Planning]** Automatic flight planning will no longer accidentally plan a recovery tanker instead of a theater refueling package. This fixes a potential crash during mission generation when opfor plans a refueling task at a sunk carrier. You'll need to skip the current turn to force opfor to replan their flights to get the fix.
* **[Mission Generation]** Using heliports (airports without any runways) will no longer cause mission generation to fail.
* **[Mission Generation]** Prevent helicopters from spawning into collisions at FARPs when more than one flight uses the same FARP.
# 6.1.0 # 6.1.0
Saves from 6.0.0 are compatible with 6.1.0 Saves from 6.0.0 are compatible with 6.1.0

View File

@@ -5,6 +5,7 @@ import operator
from collections.abc import Iterable, Iterator from collections.abc import Iterable, Iterator
from typing import TYPE_CHECKING, TypeVar from typing import TYPE_CHECKING, TypeVar
from game.ato.closestairfields import ClosestAirfields, ObjectiveDistanceCache
from game.theater import ( from game.theater import (
Airfield, Airfield,
ControlPoint, ControlPoint,
@@ -15,12 +16,11 @@ from game.theater import (
) )
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
BuildingGroundObject, BuildingGroundObject,
IadsBuildingGroundObject,
IadsGroundObject, IadsGroundObject,
NavalGroundObject, NavalGroundObject,
IadsBuildingGroundObject,
) )
from game.utils import meters, nautical_miles from game.utils import meters, nautical_miles
from game.ato.closestairfields import ClosestAirfields, ObjectiveDistanceCache
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@@ -209,22 +209,20 @@ class ObjectiveFinder:
raise RuntimeError("Found no friendly control points. You probably lost.") raise RuntimeError("Found no friendly control points. You probably lost.")
return farthest return farthest
def closest_friendly_control_point(self) -> ControlPoint: def preferred_theater_refueling_control_point(self) -> ControlPoint | None:
"""Finds the friendly control point that is closest to any threats.""" """Finds the friendly control point that is closest to any threats."""
threat_zones = self.game.threat_zone_for(not self.is_player) threat_zones = self.game.threat_zone_for(not self.is_player)
closest = None closest = None
min_distance = meters(math.inf) min_distance = meters(math.inf)
for cp in self.friendly_control_points(): for cp in self.friendly_control_points():
if isinstance(cp, OffMapSpawn): if isinstance(cp, OffMapSpawn) or cp.is_fleet:
continue continue
distance = threat_zones.distance_to_threat(cp.position) distance = threat_zones.distance_to_threat(cp.position)
if distance < min_distance: if distance < min_distance:
closest = cp closest = cp
min_distance = distance min_distance = distance
if closest is None:
raise RuntimeError("Found no friendly control points. You probably lost.")
return closest return closest
def enemy_control_points(self) -> Iterator[ControlPoint]: def enemy_control_points(self) -> Iterator[ControlPoint]:

View File

@@ -153,6 +153,11 @@ class TheaterState(WorldState["TheaterState"]):
barcap_duration = coalition.doctrine.cap_duration.total_seconds() barcap_duration = coalition.doctrine.cap_duration.total_seconds()
barcap_rounds = math.ceil(mission_duration / barcap_duration) barcap_rounds = math.ceil(mission_duration / barcap_duration)
refueling_targets: list[MissionTarget] = []
theater_refuling_point = finder.preferred_theater_refueling_control_point()
if theater_refuling_point is not None:
refueling_targets.append(theater_refuling_point)
return TheaterState( return TheaterState(
context=context, context=context,
barcaps_needed={ barcaps_needed={
@@ -162,7 +167,7 @@ class TheaterState(WorldState["TheaterState"]):
front_line_stances={f: None for f in finder.front_lines()}, front_line_stances={f: None for f in finder.front_lines()},
vulnerable_front_lines=list(finder.front_lines()), vulnerable_front_lines=list(finder.front_lines()),
aewc_targets=[finder.farthest_friendly_control_point()], aewc_targets=[finder.farthest_friendly_control_point()],
refueling_targets=[finder.closest_friendly_control_point()], refueling_targets=refueling_targets,
enemy_air_defenses=list(finder.enemy_air_defenses()), enemy_air_defenses=list(finder.enemy_air_defenses()),
threatening_air_defenses=[], threatening_air_defenses=[],
detecting_air_defenses=[], detecting_air_defenses=[],

View File

@@ -26,6 +26,7 @@ from game.settings import Settings
from game.theater.controlpoint import ( from game.theater.controlpoint import (
Airfield, Airfield,
ControlPoint, ControlPoint,
Fob,
) )
from game.unitmap import UnitMap from game.unitmap import UnitMap
from .aircraftpainter import AircraftPainter from .aircraftpainter import AircraftPainter
@@ -180,4 +181,12 @@ class AircraftGenerator:
self.unit_map, self.unit_map,
).configure() ).configure()
) )
wpt = group.waypoint("LANDING")
if flight.is_helo and isinstance(flight.arrival, Fob) and wpt:
hpad = self.helipads[flight.arrival].units.pop(0)
wpt.helipad_id = hpad.id
wpt.link_unit = hpad.id
self.helipads[flight.arrival].units.append(hpad)
return group return group

View File

@@ -230,16 +230,22 @@ class FlightGroupSpawner:
def _generate_at_cp_helipad(self, name: str, cp: ControlPoint) -> FlyingGroup[Any]: def _generate_at_cp_helipad(self, name: str, cp: ControlPoint) -> FlyingGroup[Any]:
try: try:
helipad = self.helipads[cp] helipad = self.helipads[cp]
except IndexError as ex: except IndexError:
raise NoParkingSlotError() raise NoParkingSlotError()
group = self._generate_at_group(name, helipad) group = self._generate_at_group(name, helipad)
if self.start_type is not StartType.COLD: if self.start_type is StartType.WARM:
group.points[0].type = "TakeOffParkingHot" group.points[0].type = "TakeOffParkingHot"
hpad = helipad.units[0]
for i in range(self.flight.count): for i in range(self.flight.count):
group.units[i].position = helipad.units[i].position group.units[i].position = hpad.position
group.units[i].heading = helipad.units[i].heading group.units[i].heading = hpad.heading
# pydcs has just `parking_id = None`, so mypy thinks str is invalid. Ought
# to fix pydcs, but that's not the kind of change we want to pull into the
# 6.1 branch, and frankly we should probably just improve pydcs's handling
# of FARPs instead.
group.units[i].parking_id = str(i + 1) # type: ignore
return group return group
def dcs_start_type(self) -> DcsStartType: def dcs_start_type(self) -> DcsStartType:

View File

@@ -31,10 +31,11 @@ class RecoveryTankerBuilder(PydcsWaypointBuilder):
for key, value in theater_objects.items(): for key, value in theater_objects.items():
# Check name and position in case there are multiple of same carrier. # Check name and position in case there are multiple of same carrier.
if name in key and value.theater_unit.position == carrier_position: if name in key and value.theater_unit.position == carrier_position:
theater_mapping = value return value.dcs_group_id
break raise RuntimeError(
assert theater_mapping is not None f"Could not find a carrier in the mission matching {name} at "
return theater_mapping.dcs_group_id f"({carrier_position.x}, {carrier_position.y})"
)
def configure_tanker_tacan(self, waypoint: MovingPoint) -> None: def configure_tanker_tacan(self, waypoint: MovingPoint) -> None:

View File

@@ -878,6 +878,11 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
) -> RunwayData: ) -> RunwayData:
... ...
def stub_runway_data(self) -> RunwayData:
return RunwayData(
self.full_name, runway_heading=Heading.from_degrees(0), runway_name=""
)
@property @property
def airdrome_id_for_landing(self) -> Optional[int]: def airdrome_id_for_landing(self) -> Optional[int]:
return None return None
@@ -1134,6 +1139,14 @@ class Airfield(ControlPoint):
conditions: Conditions, conditions: Conditions,
dynamic_runways: Dict[str, RunwayData], dynamic_runways: Dict[str, RunwayData],
) -> RunwayData: ) -> RunwayData:
if not self.airport.runways:
# Some airfields are heliports and don't have any runways. This isn't really
# the best fix, since we should still try to generate partial data for TACAN
# beacons, but it'll do for a bug fix, and the proper fix probably involves
# making heliports their own CP type.
# https://github.com/dcs-liberation/dcs_liberation/issues/2710
return self.stub_runway_data()
assigner = RunwayAssigner(conditions) assigner = RunwayAssigner(conditions)
return assigner.get_preferred_runway(theater, self.airport) return assigner.get_preferred_runway(theater, self.airport)
@@ -1269,8 +1282,6 @@ class Carrier(NavalControlPoint):
return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.CARRIER return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.CARRIER
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from game.ato import FlightType
yield from super().mission_types(for_player) yield from super().mission_types(for_player)
if self.is_friendly(for_player): if self.is_friendly(for_player):
yield from [ yield from [
@@ -1375,9 +1386,7 @@ class OffMapSpawn(ControlPoint):
dynamic_runways: Dict[str, RunwayData], dynamic_runways: Dict[str, RunwayData],
) -> RunwayData: ) -> RunwayData:
logging.warning("TODO: Off map spawns have no runways.") logging.warning("TODO: Off map spawns have no runways.")
return RunwayData( return self.stub_runway_data()
self.full_name, runway_heading=Heading.from_degrees(0), runway_name=""
)
@property @property
def runway_status(self) -> RunwayStatus: def runway_status(self) -> RunwayStatus:
@@ -1419,9 +1428,7 @@ class Fob(ControlPoint):
dynamic_runways: Dict[str, RunwayData], dynamic_runways: Dict[str, RunwayData],
) -> RunwayData: ) -> RunwayData:
logging.warning("TODO: FOBs have no runways.") logging.warning("TODO: FOBs have no runways.")
return RunwayData( return self.stub_runway_data()
self.full_name, runway_heading=Heading.from_degrees(0), runway_name=""
)
@property @property
def runway_status(self) -> RunwayStatus: def runway_status(self) -> RunwayStatus:

View File

@@ -3,7 +3,7 @@ from pathlib import Path
MAJOR_VERSION = 6 MAJOR_VERSION = 6
MINOR_VERSION = 1 MINOR_VERSION = 1
MICRO_VERSION = 0 MICRO_VERSION = 1
VERSION_NUMBER = ".".join(str(v) for v in (MAJOR_VERSION, MINOR_VERSION, MICRO_VERSION)) VERSION_NUMBER = ".".join(str(v) for v in (MAJOR_VERSION, MINOR_VERSION, MICRO_VERSION))