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]** 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]** 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]** 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 ## Fixes

View File

@ -115,7 +115,7 @@ class DefaultSquadronAssigner:
) -> bool: ) -> bool:
if ignore_base_preference: if ignore_base_preference:
return control_point.can_operate(squadron.aircraft) 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( def find_squadron_for_airframe(
self, aircraft: AircraftType, task: FlightType, control_point: ControlPoint self, aircraft: AircraftType, task: FlightType, control_point: ControlPoint

View File

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

View File

@ -2,11 +2,11 @@ from __future__ import annotations
import itertools import itertools
from collections import defaultdict 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.ai_flight_planner_db import aircraft_for_task
from game.ato.closestairfields import ObjectiveDistanceCache from game.ato.closestairfields import ObjectiveDistanceCache
from game.dcs.aircrafttype import AircraftType
from .squadrondefloader import SquadronDefLoader from .squadrondefloader import SquadronDefLoader
from ..campaignloader.squadrondefgenerator import SquadronDefGenerator from ..campaignloader.squadrondefgenerator import SquadronDefGenerator
from ..factions.faction import Faction from ..factions.faction import Faction
@ -82,7 +82,7 @@ class AirWing:
best_aircraft_for_task = aircraft_for_task(task) best_aircraft_for_task = aircraft_for_task(task)
for aircraft, squadrons in self.squadrons.items(): for aircraft, squadrons in self.squadrons.items():
for squadron in squadrons: 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) aircrafts.append(aircraft)
if aircraft not in best_aircraft_for_task: if aircraft not in best_aircraft_for_task:
best_aircraft_for_task.append(aircraft) 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.ato import Flight, FlightType, Package
from game.settings import AutoAtoBehavior, Settings from game.settings import AutoAtoBehavior, Settings
from .pilot import Pilot, PilotStatus from .pilot import Pilot, PilotStatus
from ..ato.ai_flight_planner_db import aircraft_for_task
from ..db.database import Database from ..db.database import Database
from ..utils import meters from ..utils import meters
@ -32,7 +33,7 @@ class Squadron:
role: str role: str
aircraft: AircraftType aircraft: AircraftType
livery: Optional[str] livery: Optional[str]
mission_types: tuple[FlightType, ...] auto_assignable_mission_types: set[FlightType]
operating_bases: OperatingBases operating_bases: OperatingBases
female_pilot_percentage: int female_pilot_percentage: int
@ -46,10 +47,6 @@ class Squadron:
default_factory=list, init=False, hash=False, compare=False 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) coalition: Coalition = field(hash=False, compare=False)
flight_db: Database[Flight] = field(hash=False, compare=False) flight_db: Database[Flight] = field(hash=False, compare=False)
settings: Settings = 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) untasked_aircraft: int = field(init=False, hash=False, compare=False, default=0)
pending_deliveries: 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: def __str__(self) -> str:
if self.nickname is None: if self.nickname is None:
return self.name return self.name
@ -94,16 +88,12 @@ class Squadron:
def pilot_limits_enabled(self) -> bool: def pilot_limits_enabled(self) -> bool:
return self.settings.enable_squadron_pilot_limits 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( def set_auto_assignable_mission_types(
self, mission_types: Iterable[FlightType] self, mission_types: Iterable[FlightType]
) -> None: ) -> None:
self.auto_assignable_mission_types = set(self.mission_types).intersection( self.auto_assignable_mission_types = {
mission_types t for t in mission_types if self.capable_of(t)
) }
def claim_new_pilot_if_allowed(self) -> Optional[Pilot]: def claim_new_pilot_if_allowed(self) -> Optional[Pilot]:
if self.pilot_limits_enabled: if self.pilot_limits_enabled:
@ -257,7 +247,20 @@ class Squadron:
def has_unfilled_pilot_slots(self) -> bool: def has_unfilled_pilot_slots(self) -> bool:
return not self.pilot_limits_enabled or self._number_of_unfilled_pilot_slots > 0 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: 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 return task in self.auto_assignable_mission_types
def can_auto_assign_mission( def can_auto_assign_mission(
@ -432,7 +435,7 @@ class Squadron:
squadron_def.role, squadron_def.role,
squadron_def.aircraft, squadron_def.aircraft,
squadron_def.livery, squadron_def.livery,
squadron_def.mission_types, squadron_def.auto_assignable_mission_types,
squadron_def.operating_bases, squadron_def.operating_bases,
squadron_def.female_pilot_percentage, squadron_def.female_pilot_percentage,
squadron_def.pilot_pool, squadron_def.pilot_pool,

View File

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

View File

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

View File

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

View File

@ -48,7 +48,7 @@ class SquadronSelector(QComboBox):
return return
for squadron in self.air_wing.squadrons_for(aircraft): 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) self.addItem(f"{squadron.location}: {squadron}", squadron)
if self.count() == 0: if self.count() == 0: