Disallow squadrons from disabling mission types.

After this change, players will always have the final say in what
missions a squadron can be assigned to. Squadrons are not able to
influence the default auto-assignable missions either because that
property is always overridden by the campaign's air wing configuration
(the primary and secondary task properties). The `mission-types` field
of the squadron definition has been removed since it is no longer
capable of influencing anything. I haven't bothered cleaning up the now
useless data in all the existing squadrons though.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2785.
This commit is contained in:
Dan Albert 2023-04-17 19:21:10 -07:00
parent 1ac36d03da
commit 94b8aa7213
9 changed files with 49 additions and 80 deletions

View File

@ -14,6 +14,8 @@ Saves from 6.x are not compatible with 7.0.
* **[Modding]** Add support for VSN F-4B and F-4C mod.
* **[Modding]** Custom factions can now be defined in YAML as well as JSON. JSON support may be removed in the future if having both formats causes confusion.
* **[Modding]** Campaigns which require custom factions can now define those factions directly in the campaign YAML. See Operation Aliied Sword for an example.
* **[Modding]** The `mission_types` field in squadron files has been removed. Squadron task capability is now determined by airframe, and the auto-assignable list has always been overridden by the campaign settings.
* **[Squadrons]** Squadron-specific mission capability lists no longer restrict players from assigning missions outside the squadron's preferences.
## Fixes

View File

@ -115,7 +115,7 @@ class DefaultSquadronAssigner:
) -> bool:
if ignore_base_preference:
return control_point.can_operate(squadron.aircraft)
return squadron.operates_from(control_point) and task in squadron.mission_types
return squadron.operates_from(control_point) and squadron.capable_of(task)
def find_squadron_for_airframe(
self, aircraft: AircraftType, task: FlightType, control_point: ControlPoint

View File

@ -4,12 +4,12 @@ import itertools
import random
from typing import Optional, TYPE_CHECKING
from game.ato.ai_flight_planner_db import aircraft_for_task, tasks_for_aircraft
from game.ato.flighttype import FlightType
from game.dcs.aircrafttype import AircraftType
from game.squadrons.operatingbases import OperatingBases
from game.squadrons.squadrondef import SquadronDef
from game.theater import ControlPoint
from game.ato.ai_flight_planner_db import aircraft_for_task, tasks_for_aircraft
if TYPE_CHECKING:
from game.factions.faction import Faction
@ -48,7 +48,7 @@ class SquadronDefGenerator:
role="Flying Squadron",
aircraft=aircraft,
livery=None,
mission_types=tuple(tasks_for_aircraft(aircraft)),
auto_assignable_mission_types=set(tasks_for_aircraft(aircraft)),
operating_bases=OperatingBases.default_for_aircraft(aircraft),
female_pilot_percentage=6,
pilot_pool=[],

View File

@ -2,11 +2,11 @@ from __future__ import annotations
import itertools
from collections import defaultdict
from typing import Sequence, Iterator, TYPE_CHECKING, Optional
from typing import Iterator, Optional, Sequence, TYPE_CHECKING
from game.dcs.aircrafttype import AircraftType
from game.ato.ai_flight_planner_db import aircraft_for_task
from game.ato.closestairfields import ObjectiveDistanceCache
from game.dcs.aircrafttype import AircraftType
from .squadrondefloader import SquadronDefLoader
from ..campaignloader.squadrondefgenerator import SquadronDefGenerator
from ..factions.faction import Faction
@ -82,7 +82,7 @@ class AirWing:
best_aircraft_for_task = aircraft_for_task(task)
for aircraft, squadrons in self.squadrons.items():
for squadron in squadrons:
if squadron.untasked_aircraft and task in squadron.mission_types:
if squadron.untasked_aircraft and squadron.capable_of(task):
aircrafts.append(aircraft)
if aircraft not in best_aircraft_for_task:
best_aircraft_for_task.append(aircraft)

View File

@ -12,6 +12,7 @@ from faker import Faker
from game.ato import Flight, FlightType, Package
from game.settings import AutoAtoBehavior, Settings
from .pilot import Pilot, PilotStatus
from ..ato.ai_flight_planner_db import aircraft_for_task
from ..db.database import Database
from ..utils import meters
@ -32,7 +33,7 @@ class Squadron:
role: str
aircraft: AircraftType
livery: Optional[str]
mission_types: tuple[FlightType, ...]
auto_assignable_mission_types: set[FlightType]
operating_bases: OperatingBases
female_pilot_percentage: int
@ -46,10 +47,6 @@ class Squadron:
default_factory=list, init=False, hash=False, compare=False
)
auto_assignable_mission_types: set[FlightType] = field(
init=False, hash=False, compare=False
)
coalition: Coalition = field(hash=False, compare=False)
flight_db: Database[Flight] = field(hash=False, compare=False)
settings: Settings = field(hash=False, compare=False)
@ -63,9 +60,6 @@ class Squadron:
untasked_aircraft: int = field(init=False, hash=False, compare=False, default=0)
pending_deliveries: int = field(init=False, hash=False, compare=False, default=0)
def __post_init__(self) -> None:
self.auto_assignable_mission_types = set(self.mission_types)
def __str__(self) -> str:
if self.nickname is None:
return self.name
@ -94,16 +88,12 @@ class Squadron:
def pilot_limits_enabled(self) -> bool:
return self.settings.enable_squadron_pilot_limits
def set_allowed_mission_types(self, mission_types: Iterable[FlightType]) -> None:
self.mission_types = tuple(mission_types)
self.auto_assignable_mission_types.intersection_update(self.mission_types)
def set_auto_assignable_mission_types(
self, mission_types: Iterable[FlightType]
) -> None:
self.auto_assignable_mission_types = set(self.mission_types).intersection(
mission_types
)
self.auto_assignable_mission_types = {
t for t in mission_types if self.capable_of(t)
}
def claim_new_pilot_if_allowed(self) -> Optional[Pilot]:
if self.pilot_limits_enabled:
@ -257,7 +247,20 @@ class Squadron:
def has_unfilled_pilot_slots(self) -> bool:
return not self.pilot_limits_enabled or self._number_of_unfilled_pilot_slots > 0
def capable_of(self, task: FlightType) -> bool:
"""Returns True if the squadron is capable of performing the given task.
A squadron may be capable of performing a task even if it will not be
automatically assigned to it.
"""
return self.aircraft in aircraft_for_task(task)
def can_auto_assign(self, task: FlightType) -> bool:
"""Returns True if the squadron may be automatically assigned the given task.
A squadron may be capable of performing a task even if it will not be
automatically assigned to it.
"""
return task in self.auto_assignable_mission_types
def can_auto_assign_mission(
@ -432,7 +435,7 @@ class Squadron:
squadron_def.role,
squadron_def.aircraft,
squadron_def.livery,
squadron_def.mission_types,
squadron_def.auto_assignable_mission_types,
squadron_def.operating_bases,
squadron_def.female_pilot_percentage,
squadron_def.pilot_pool,

View File

@ -1,13 +1,12 @@
from __future__ import annotations
import logging
from collections.abc import Iterable
from dataclasses import dataclass, field
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, TYPE_CHECKING
import yaml
from game.ato.ai_flight_planner_db import aircraft_for_task, tasks_for_aircraft
from game.dcs.aircrafttype import AircraftType
from game.squadrons.operatingbases import OperatingBases
from game.squadrons.pilot import Pilot
@ -25,30 +24,24 @@ class SquadronDef:
role: str
aircraft: AircraftType
livery: Optional[str]
mission_types: tuple[FlightType, ...]
auto_assignable_mission_types: set[FlightType]
operating_bases: OperatingBases
female_pilot_percentage: int
pilot_pool: list[Pilot]
claimed: bool = False
auto_assignable_mission_types: set[FlightType] = field(
init=False, hash=False, compare=False
)
def __post_init__(self) -> None:
self.auto_assignable_mission_types = set(self.mission_types)
def __str__(self) -> str:
if self.nickname is None:
return self.name
return f'{self.name} "{self.nickname}"'
def set_allowed_mission_types(self, mission_types: Iterable[FlightType]) -> None:
self.mission_types = tuple(mission_types)
self.auto_assignable_mission_types.intersection_update(self.mission_types)
def capable_of(self, task: FlightType) -> bool:
"""Returns True if the squadron is capable of performing the given task.
def can_auto_assign(self, task: FlightType) -> bool:
return task in self.auto_assignable_mission_types
A squadron may be capable of performing a task even if it will not be
automatically assigned to it.
"""
return self.aircraft in aircraft_for_task(task)
def operates_from(self, control_point: ControlPoint) -> bool:
if not control_point.can_operate(self.aircraft):
@ -62,8 +55,6 @@ class SquadronDef:
@classmethod
def from_yaml(cls, path: Path) -> SquadronDef:
from game.ato.ai_flight_planner_db import tasks_for_aircraft
from game.ato import FlightType
with path.open(encoding="utf8") as squadron_file:
data = yaml.safe_load(squadron_file)
@ -78,16 +69,6 @@ class SquadronDef:
pilots.extend([Pilot(n, player=True) for n in data.get("players", [])])
female_pilot_percentage = data.get("female_pilot_percentage", 6)
mission_types = [FlightType.from_name(n) for n in data["mission_types"]]
tasks = tasks_for_aircraft(unit_type)
for mission_type in list(mission_types):
if mission_type not in tasks:
logging.error(
f"Squadron has mission type {mission_type} but {unit_type} is not "
f"capable of that task: {path}"
)
mission_types.remove(mission_type)
return SquadronDef(
name=data["name"],
nickname=data.get("nickname"),
@ -95,7 +76,7 @@ class SquadronDef:
role=data["role"],
aircraft=unit_type,
livery=data.get("livery"),
mission_types=tuple(mission_types),
auto_assignable_mission_types=set(tasks_for_aircraft(unit_type)),
operating_bases=OperatingBases.from_yaml(unit_type, data.get("bases", {})),
female_pilot_percentage=female_pilot_percentage,
pilot_pool=pilots,

View File

@ -44,9 +44,6 @@ class QMissionType:
self, mission_type: FlightType, allowed: bool, auto_assignable: bool
) -> None:
self.flight_type = mission_type
self.allowed_checkbox = QCheckBox()
self.allowed_checkbox.setChecked(allowed)
self.allowed_checkbox.toggled.connect(self.update_auto_assignable)
self.auto_assignable_checkbox = QCheckBox()
self.auto_assignable_checkbox.setEnabled(allowed)
self.auto_assignable_checkbox.setChecked(auto_assignable)
@ -56,10 +53,6 @@ class QMissionType:
if not checked:
self.auto_assignable_checkbox.setChecked(False)
@property
def allowed(self) -> bool:
return self.allowed_checkbox.isChecked()
@property
def auto_assignable(self) -> bool:
return self.auto_assignable_checkbox.isChecked()
@ -72,27 +65,20 @@ class MissionTypeControls(QGridLayout):
self.mission_types: list[QMissionType] = []
self.addWidget(QLabel("Mission Type"), 0, 0)
self.addWidget(QLabel("Allow"), 0, 1)
self.addWidget(QLabel("Auto-Assign"), 0, 2)
self.addWidget(QLabel("Auto-Assign"), 0, 1)
for i, task in enumerate(FlightType):
if task is FlightType.FERRY:
# Not plannable so just skip it.
continue
allowed = task in squadron.mission_types
auto_assignable = task in squadron.auto_assignable_mission_types
mission_type = QMissionType(task, allowed, auto_assignable)
mission_type = QMissionType(
task, squadron.capable_of(task), auto_assignable
)
self.mission_types.append(mission_type)
self.addWidget(QLabel(task.value), i + 1, 0)
self.addWidget(mission_type.allowed_checkbox, i + 1, 1)
self.addWidget(mission_type.auto_assignable_checkbox, i + 1, 2)
@property
def allowed_mission_types(self) -> Iterator[FlightType]:
for mission_type in self.mission_types:
if mission_type.allowed:
yield mission_type.flight_type
self.addWidget(mission_type.auto_assignable_checkbox, i + 1, 1)
@property
def auto_assignable_mission_types(self) -> Iterator[FlightType]:
@ -233,10 +219,6 @@ class SquadronConfigurationBox(QGroupBox):
self.squadron.pilot_pool = [
Pilot(n, player=True) for n in player_names
] + self.squadron.pilot_pool
# Set the allowed mission types
self.squadron.set_allowed_mission_types(
set(self.mission_types.allowed_mission_types)
)
# Also update the auto assignable mission types
self.squadron.set_auto_assignable_mission_types(
set(self.mission_types.auto_assignable_mission_types)

View File

@ -77,11 +77,12 @@ class AutoAssignedTaskControls(QVBoxLayout):
return callback
for task in squadron_model.squadron.mission_types:
checkbox = QCheckBox(text=task.value)
checkbox.setChecked(squadron_model.is_auto_assignable(task))
checkbox.toggled.connect(make_callback(task))
self.addWidget(checkbox)
for task in FlightType:
if self.squadron_model.squadron.capable_of(task):
checkbox = QCheckBox(text=task.value)
checkbox.setChecked(squadron_model.is_auto_assignable(task))
checkbox.toggled.connect(make_callback(task))
self.addWidget(checkbox)
self.addStretch()

View File

@ -48,7 +48,7 @@ class SquadronSelector(QComboBox):
return
for squadron in self.air_wing.squadrons_for(aircraft):
if task in squadron.mission_types and squadron.untasked_aircraft:
if squadron.capable_of(task) and squadron.untasked_aircraft:
self.addItem(f"{squadron.location}: {squadron}", squadron)
if self.count() == 0: