Don't propose missions the air wing can't plan.

We were doignt his for escorts, but now that we quit planning as soon as
we find an unplannable mission (to save money for higher priority
missions), if we hit an early unplannable mission like BARCAP no other
missions wil be planned.

Maybe fixes https://github.com/dcs-liberation/dcs_liberation/issues/1228
This commit is contained in:
Dan Albert 2021-06-19 11:44:56 -07:00
parent dc4794b246
commit 3338df9836
2 changed files with 95 additions and 61 deletions

View File

@ -19,6 +19,7 @@ Saves from 3.x are not compatible with 4.0.
## Fixes ## Fixes
* **[Campaign AI]** Fix procurement for factions that lack some unit types. * **[Campaign AI]** Fix procurement for factions that lack some unit types.
* **[Campaign AI]** Improved pruning of unplannable missions which should improve turn cycle time and prevent the auto-planner from quitting early.
* **[Mission Generation]** Fixed problem with mission load when control point name contained an apostrophe. * **[Mission Generation]** Fixed problem with mission load when control point name contained an apostrophe.
* **[Mission Generation]** Fixed EWR group names so they contribute to Skynet again. * **[Mission Generation]** Fixed EWR group names so they contribute to Skynet again.
* **[Mission Generation]** Fixed duplicate name error when generating convoys and cargo ships when creating manual transfers after loading a game. * **[Mission Generation]** Fixed duplicate name error when generating convoys and cargo ships when creating manual transfers after loading a game.

View File

@ -613,33 +613,18 @@ class CoalitionMissionPlanner:
return True return True
return False return False
def critical_missions(self) -> Iterator[ProposedMission]: @property
"""Identifies the most important missions to plan this turn. def oca_aircraft_plannable(self) -> bool:
return (
Non-critical missions that cannot be fulfilled will create purchase self.air_wing_can_plan(FlightType.OCA_AIRCRAFT)
orders for the next turn. Critical missions will create a purchase order and self.game.settings.default_start_type == "Cold"
unless the mission can be doubly fulfilled. In other words, the AI will
attempt to have *double* the aircraft it needs for these missions to
ensure that they can be planned again next turn even if all aircraft are
eliminated this turn.
"""
# Find farthest, friendly CP for AEWC.
yield ProposedMission(
self.objective_finder.farthest_friendly_control_point(),
[ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)],
# Supports all the early CAP flights, so should be in the air ASAP.
asap=True,
)
yield ProposedMission(
self.objective_finder.closest_friendly_control_point(),
[ProposedFlight(FlightType.REFUELING, 1, self.MAX_TANKER_RANGE)],
) )
def propose_barcap(self) -> Iterator[ProposedMission]:
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP. # Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
for cp in self.objective_finder.vulnerable_control_points(): for cp in self.objective_finder.vulnerable_control_points():
# Plan CAP in such a way, that it is established during the whole desired mission length # Plan CAP in such a way, that it is established during the whole desired
# mission length.
for _ in range( for _ in range(
0, 0,
int(self.game.settings.desired_player_mission_duration.total_seconds()), int(self.game.settings.desired_player_mission_duration.total_seconds()),
@ -652,36 +637,31 @@ class CoalitionMissionPlanner:
], ],
) )
def propose_cas(self) -> Iterator[ProposedMission]:
# Find front lines, plan CAS. # Find front lines, plan CAS.
for front_line in self.objective_finder.front_lines(): for front_line in self.objective_finder.front_lines():
yield ProposedMission( flights = [ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE)]
front_line, if self.air_wing_can_plan(FlightType.TARCAP):
[ # This is *not* an escort because front lines don't create a threat
ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE), # zone. Generating threat zones from front lines causes the front
# This is *not* an escort because front lines don't create a threat # line to push back BARCAPs as it gets closer to the base. While
# zone. Generating threat zones from front lines causes the front # front lines do have the same problem of potentially pulling
# line to push back BARCAPs as it gets closer to the base. While # BARCAPs off bases to engage a front line TARCAP, that's probably
# front lines do have the same problem of potentially pulling # the one time where we do want that.
# BARCAPs off bases to engage a front line TARCAP, that's probably #
# the one time where we do want that. # TODO: Use intercepts and extra TARCAPs to cover bases near fronts.
# # We don't have intercept missions yet so this isn't something we
# TODO: Use intercepts and extra TARCAPs to cover bases near fronts. # can do today, but we should probably return to having the front
# We don't have intercept missions yet so this isn't something we # line project a threat zone (so that strike missions will route
# can do today, but we should probably return to having the front # around it) and instead *not plan* a BARCAP at bases near the
# line project a threat zone (so that strike missions will route # front, since there isn't a place to put a barrier. Instead, the
# around it) and instead *not plan* a BARCAP at bases near the # aircraft that would have been a BARCAP could be used as additional
# front, since there isn't a place to put a barrier. Instead, the # interceptors and TARCAPs which will defend the base but won't be
# aircraft that would have been a BARCAP could be used as additional # trying to avoid front line contacts.
# interceptors and TARCAPs which will defend the base but won't be flights.append(ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE))
# trying to avoid front line contacts. yield ProposedMission(front_line, flights)
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE),
],
)
def propose_missions(self) -> Iterator[ProposedMission]:
"""Identifies and iterates over potential mission in priority order."""
yield from self.critical_missions()
def propose_dead(self) -> Iterator[ProposedMission]:
# Find enemy SAM sites with ranges that cover friendly CPs, front lines, # Find enemy SAM sites with ranges that cover friendly CPs, front lines,
# or objects, plan DEAD. # or objects, plan DEAD.
# Find enemy SAM sites with ranges that extend to within 50 nmi of # Find enemy SAM sites with ranges that extend to within 50 nmi of
@ -706,7 +686,10 @@ class CoalitionMissionPlanner:
else: else:
flights.append( flights.append(
ProposedFlight( ProposedFlight(
FlightType.SEAD_ESCORT, 2, self.MAX_SEAD_RANGE, EscortType.Sead FlightType.SEAD_ESCORT,
2,
self.MAX_SEAD_RANGE,
EscortType.Sead,
) )
) )
# TODO: Max escort range. # TODO: Max escort range.
@ -717,6 +700,7 @@ class CoalitionMissionPlanner:
) )
yield ProposedMission(sam, flights) yield ProposedMission(sam, flights)
def propose_convoy_interdiction(self) -> Iterator[ProposedMission]:
# These will only rarely get planned. When a convoy is travelling multiple legs, # These will only rarely get planned. When a convoy is travelling multiple legs,
# they're targetable after the first leg. The reason for this is that # they're targetable after the first leg. The reason for this is that
# procurement happens *after* mission planning so that the missions that could # procurement happens *after* mission planning so that the missions that could
@ -745,6 +729,7 @@ class CoalitionMissionPlanner:
], ],
) )
def propose_shipping_interdiction(self) -> Iterator[ProposedMission]:
for ship in self.objective_finder.cargo_ships(): for ship in self.objective_finder.cargo_ships():
yield ProposedMission( yield ProposedMission(
ship, ship,
@ -760,6 +745,7 @@ class CoalitionMissionPlanner:
], ],
) )
def propose_naval_strikes(self) -> Iterator[ProposedMission]:
for group in self.objective_finder.threatening_ships(): for group in self.objective_finder.threatening_ships():
yield ProposedMission( yield ProposedMission(
group, group,
@ -775,6 +761,7 @@ class CoalitionMissionPlanner:
], ],
) )
def propose_bai(self) -> Iterator[ProposedMission]:
for group in self.objective_finder.threatening_vehicle_groups(): for group in self.objective_finder.threatening_vehicle_groups():
yield ProposedMission( yield ProposedMission(
group, group,
@ -790,16 +777,25 @@ class CoalitionMissionPlanner:
], ],
) )
def propose_oca_strikes(self) -> Iterator[ProposedMission]:
for target in self.objective_finder.oca_targets(min_aircraft=20): for target in self.objective_finder.oca_targets(min_aircraft=20):
flights = [ flights = []
ProposedFlight(FlightType.OCA_RUNWAY, 2, self.MAX_OCA_RANGE), if self.air_wing_can_plan(FlightType.OCA_RUNWAY):
] flights.append(
if self.game.settings.default_start_type == "Cold": ProposedFlight(FlightType.OCA_RUNWAY, 2, self.MAX_OCA_RANGE)
)
if self.oca_aircraft_plannable:
# Only schedule if the default start type is Cold. If the player # Only schedule if the default start type is Cold. If the player
# has set anything else there are no targets to hit. # has set anything else there are no targets to hit.
flights.append( flights.append(
ProposedFlight(FlightType.OCA_AIRCRAFT, 2, self.MAX_OCA_RANGE) ProposedFlight(FlightType.OCA_AIRCRAFT, 2, self.MAX_OCA_RANGE)
) )
if not flights:
raise RuntimeError(
"Attempted planning of OCA strikes but neither OCA/Runway nor "
f"OCA/Aircraft are plannable for {self.faction.name} with the "
"current game settings."
)
flights.extend( flights.extend(
[ [
# TODO: Max escort range. # TODO: Max escort range.
@ -813,7 +809,7 @@ class CoalitionMissionPlanner:
) )
yield ProposedMission(target, flights) yield ProposedMission(target, flights)
# Plan strike missions. def propose_building_strikes(self) -> Iterator[ProposedMission]:
for target in self.objective_finder.strike_targets(): for target in self.objective_finder.strike_targets():
yield ProposedMission( yield ProposedMission(
target, target,
@ -832,6 +828,48 @@ class CoalitionMissionPlanner:
], ],
) )
def propose_missions(self) -> Iterator[ProposedMission]:
"""Identifies and iterates over potential mission in priority order."""
# Find farthest, friendly CP for AEWC.
if self.air_wing_can_plan(FlightType.AEWC):
yield ProposedMission(
self.objective_finder.farthest_friendly_control_point(),
[ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)],
# Supports all the early CAP flights, so should be in the air ASAP.
asap=True,
)
if self.air_wing_can_plan(FlightType.REFUELING):
yield ProposedMission(
self.objective_finder.closest_friendly_control_point(),
[ProposedFlight(FlightType.REFUELING, 1, self.MAX_TANKER_RANGE)],
)
if self.air_wing_can_plan(FlightType.BARCAP):
yield from self.propose_barcap()
if self.air_wing_can_plan(FlightType.CAS):
yield from self.propose_cas()
if self.air_wing_can_plan(FlightType.DEAD):
yield from self.propose_dead()
if self.air_wing_can_plan(FlightType.BAI):
yield from self.propose_convoy_interdiction()
if self.air_wing_can_plan(FlightType.ANTISHIP):
yield from self.propose_shipping_interdiction()
yield from self.propose_naval_strikes()
if self.air_wing_can_plan(FlightType.BAI):
yield from self.propose_bai()
if self.air_wing_can_plan(FlightType.OCA_RUNWAY) or self.oca_aircraft_plannable:
yield from self.propose_oca_strikes()
if self.air_wing_can_plan(FlightType.STRIKE):
yield from self.propose_building_strikes()
def plan_missions(self) -> None: def plan_missions(self) -> None:
"""Identifies and plans mission for the turn.""" """Identifies and plans mission for the turn."""
player = "Blue" if self.is_player else "Red" player = "Blue" if self.is_player else "Red"
@ -840,11 +878,6 @@ class CoalitionMissionPlanner:
for proposed_mission in self.propose_missions(): for proposed_mission in self.propose_missions():
self.plan_mission(proposed_mission, tracer) self.plan_mission(proposed_mission, tracer)
with logged_duration(f"{player} reserve mission planning"):
with MultiEventTracer() as tracer:
for critical_mission in self.critical_missions():
self.plan_mission(critical_mission, tracer, reserves=True)
with logged_duration(f"{player} mission scheduling"): with logged_duration(f"{player} mission scheduling"):
self.stagger_missions() self.stagger_missions()