Convert flight creator to pilot roster.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1143
This commit is contained in:
Dan Albert 2021-06-02 01:48:39 -07:00
parent 8edb952800
commit 558502d8ea
9 changed files with 147 additions and 71 deletions

View File

@ -144,7 +144,7 @@ class Event:
def _commit_pilot_experience(ato: AirTaskingOrder) -> None: def _commit_pilot_experience(ato: AirTaskingOrder) -> None:
for package in ato.packages: for package in ato.packages:
for flight in package.flights: for flight in package.flights:
for idx, pilot in enumerate(flight.pilots): for idx, pilot in enumerate(flight.roster.pilots):
if pilot is None: if pilot is None:
logging.error( logging.error(
f"Cannot award experience to pilot #{idx} of {flight} " f"Cannot award experience to pilot #{idx} of {flight} "

View File

@ -13,7 +13,6 @@ from typing import (
List, List,
TYPE_CHECKING, TYPE_CHECKING,
Optional, Optional,
Iterable,
Iterator, Iterator,
Sequence, Sequence,
) )
@ -142,8 +141,12 @@ class Squadron:
def return_pilot(self, pilot: Pilot) -> None: def return_pilot(self, pilot: Pilot) -> None:
self.available_pilots.append(pilot) self.available_pilots.append(pilot)
def return_pilots(self, pilots: Iterable[Pilot]) -> None: def return_pilots(self, pilots: Sequence[Pilot]) -> None:
self.available_pilots.extend(pilots) # Return in reverse so that returning two pilots and then getting two more
# results in the same ordering. This happens commonly when resetting rosters in
# the UI, when we clear the roster because the UI is updating, then end up
# repopulating the same size flight from the same squadron.
self.available_pilots.extend(reversed(pilots))
def enlist_new_pilots(self, count: int) -> None: def enlist_new_pilots(self, count: int) -> None:
new_pilots = [Pilot(self.faker.name()) for _ in range(count)] new_pilots = [Pilot(self.faker.name()) for _ in range(count)]

View File

@ -62,7 +62,7 @@ class UnitMap:
self.airlifts: Dict[str, AirliftUnit] = {} self.airlifts: Dict[str, AirliftUnit] = {}
def add_aircraft(self, group: FlyingGroup, flight: Flight) -> None: def add_aircraft(self, group: FlyingGroup, flight: Flight) -> None:
for pilot, unit in zip(flight.pilots, group.units): for pilot, unit in zip(flight.roster.pilots, group.units):
# The actual name is a String (the pydcs translatable string), which # The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__. # doesn't define __eq__.
name = str(unit.name) name = str(unit.name)

View File

@ -803,7 +803,7 @@ class AircraftConflictGenerator:
self._setup_payload(flight, group) self._setup_payload(flight, group)
self._setup_livery(flight, group) self._setup_livery(flight, group)
for unit, pilot in zip(group.units, flight.pilots): for unit, pilot in zip(group.units, flight.roster.pilots):
player = pilot is not None and pilot.player player = pilot is not None and pilot.player
self.set_skill(unit, pilot, blue=flight.departure.captured) self.set_skill(unit, pilot, blue=flight.departure.captured)
# Do not generate player group with late activation. # Do not generate player group with late activation.

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from enum import Enum from enum import Enum
from typing import List, Optional, TYPE_CHECKING, Type, Union from typing import List, Optional, TYPE_CHECKING, Type, Union
@ -200,6 +201,49 @@ class FlightWaypoint:
return waypoint return waypoint
class FlightRoster:
def __init__(self, squadron: Squadron, initial_size: int = 0) -> None:
self.squadron = squadron
self.pilots: list[Optional[Pilot]] = []
self.resize(initial_size)
@property
def max_size(self) -> int:
return len(self.pilots)
@property
def player_count(self) -> int:
return len([p for p in self.pilots if p is not None and p.player])
@property
def missing_pilots(self) -> int:
return len([p for p in self.pilots if p is None])
def resize(self, new_size: int) -> None:
if self.max_size > new_size:
self.squadron.return_pilots(
[p for p in self.pilots[new_size:] if p is not None]
)
self.pilots = self.pilots[:new_size]
return
self.pilots.extend(
[
self.squadron.claim_available_pilot()
for _ in range(new_size - self.max_size)
]
)
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
if pilot is not None:
self.squadron.claim_pilot(pilot)
if (current_pilot := self.pilots[index]) is not None:
self.squadron.return_pilot(current_pilot)
self.pilots[index] = pilot
def clear(self) -> None:
self.squadron.return_pilots([p for p in self.pilots if p is not None])
class Flight: class Flight:
def __init__( def __init__(
self, self,
@ -214,11 +258,15 @@ class Flight:
divert: Optional[ControlPoint], divert: Optional[ControlPoint],
custom_name: Optional[str] = None, custom_name: Optional[str] = None,
cargo: Optional[TransferOrder] = None, cargo: Optional[TransferOrder] = None,
roster: Optional[FlightRoster] = None,
) -> None: ) -> None:
self.package = package self.package = package
self.country = country self.country = country
self.squadron = squadron self.squadron = squadron
self.pilots = [squadron.claim_available_pilot() for _ in range(count)] if roster is None:
self.roster = FlightRoster(self.squadron, initial_size=count)
else:
self.roster = roster
self.departure = departure self.departure = departure
self.arrival = arrival self.arrival = arrival
self.divert = divert self.divert = divert
@ -244,11 +292,11 @@ class Flight:
@property @property
def count(self) -> int: def count(self) -> int:
return len(self.pilots) return self.roster.max_size
@property @property
def client_count(self) -> int: def client_count(self) -> int:
return len([p for p in self.pilots if p is not None and p.player]) return self.roster.player_count
@property @property
def unit_type(self) -> Type[FlyingType]: def unit_type(self) -> Type[FlyingType]:
@ -263,32 +311,17 @@ class Flight:
return self.flight_plan.waypoints[1:] return self.flight_plan.waypoints[1:]
def resize(self, new_size: int) -> None: def resize(self, new_size: int) -> None:
if self.count > new_size: self.roster.resize(new_size)
self.squadron.return_pilots(
p for p in self.pilots[new_size:] if p is not None
)
self.pilots = self.pilots[:new_size]
return
self.pilots.extend(
[
self.squadron.claim_available_pilot()
for _ in range(new_size - self.count)
]
)
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None: def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
if pilot is not None: self.roster.set_pilot(index, pilot)
self.squadron.claim_pilot(pilot)
if (current_pilot := self.pilots[index]) is not None:
self.squadron.return_pilot(current_pilot)
self.pilots[index] = pilot
@property @property
def missing_pilots(self) -> int: def missing_pilots(self) -> int:
return len([p for p in self.pilots if p is None]) return self.roster.missing_pilots
def clear_roster(self) -> None: def clear_roster(self) -> None:
self.squadron.return_pilots([p for p in self.pilots if p is not None]) self.roster.clear()
def __repr__(self): def __repr__(self):
name = db.unit_type_name(self.unit_type) name = db.unit_type_name(self.unit_type)

View File

@ -115,7 +115,7 @@ class AircraftInventoryData:
flight_type = flight.flight_type.value flight_type = flight.flight_type.value
target = flight.package.target.name target = flight.package.target.name
for idx in range(0, num_units): for idx in range(0, num_units):
pilot = flight.pilots[idx] pilot = flight.roster.pilots[idx]
if pilot is None: if pilot is None:
pilot_name = "Unassigned" pilot_name = "Unassigned"
player = "" player = ""

View File

@ -10,6 +10,7 @@ from PySide2.QtWidgets import (
QPushButton, QPushButton,
QVBoxLayout, QVBoxLayout,
QLineEdit, QLineEdit,
QHBoxLayout,
) )
from dcs.unittype import FlyingType from dcs.unittype import FlyingType
@ -17,7 +18,7 @@ from game import Game
from game.squadrons import Squadron from game.squadrons import Squadron
from game.theater import ControlPoint, OffMapSpawn from game.theater import ControlPoint, OffMapSpawn
from gen.ato import Package from gen.ato import Package
from gen.flights.flight import Flight from gen.flights.flight import Flight, FlightRoster
from qt_ui.uiconstants import EVENT_ICONS from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.QFlightSizeSpinner import QFlightSizeSpinner from qt_ui.widgets.QFlightSizeSpinner import QFlightSizeSpinner
from qt_ui.widgets.QLabeledWidget import QLabeledWidget from qt_ui.widgets.QLabeledWidget import QLabeledWidget
@ -26,6 +27,7 @@ from qt_ui.widgets.combos.QArrivalAirfieldSelector import QArrivalAirfieldSelect
from qt_ui.widgets.combos.QFlightTypeComboBox import QFlightTypeComboBox from qt_ui.widgets.combos.QFlightTypeComboBox import QFlightTypeComboBox
from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector
from qt_ui.windows.mission.flight.SquadronSelector import SquadronSelector from qt_ui.windows.mission.flight.SquadronSelector import SquadronSelector
from qt_ui.windows.mission.flight.settings.QFlightSlotEditor import FlightRosterEditor
class QFlightCreator(QDialog): class QFlightCreator(QDialog):
@ -46,7 +48,7 @@ class QFlightCreator(QDialog):
self.task_selector = QFlightTypeComboBox(self.game.theater, package.target) self.task_selector = QFlightTypeComboBox(self.game.theater, package.target)
self.task_selector.setCurrentIndex(0) self.task_selector.setCurrentIndex(0)
self.task_selector.currentTextChanged.connect(self.on_task_changed) self.task_selector.currentIndexChanged.connect(self.on_task_changed)
layout.addLayout(QLabeledWidget("Task:", self.task_selector)) layout.addLayout(QLabeledWidget("Task:", self.task_selector))
self.aircraft_selector = QAircraftTypeSelector( self.aircraft_selector = QAircraftTypeSelector(
@ -93,13 +95,20 @@ class QFlightCreator(QDialog):
self.update_max_size(self.departure.available) self.update_max_size(self.departure.available)
layout.addLayout(QLabeledWidget("Size:", self.flight_size_spinner)) layout.addLayout(QLabeledWidget("Size:", self.flight_size_spinner))
self.client_slots_spinner = QFlightSizeSpinner( squadron = self.squadron_selector.currentData()
min_size=0, max_size=self.flight_size_spinner.value(), default_size=0 if squadron is None:
) roster = None
self.flight_size_spinner.valueChanged.connect( else:
lambda v: self.client_slots_spinner.setMaximum(v) roster = FlightRoster(
) squadron, initial_size=self.flight_size_spinner.value()
layout.addLayout(QLabeledWidget("Client Slots:", self.client_slots_spinner)) )
self.roster_editor = FlightRosterEditor(roster)
self.flight_size_spinner.valueChanged.connect(self.resize_roster)
self.squadron_selector.currentIndexChanged.connect(self.on_squadron_changed)
roster_layout = QHBoxLayout()
layout.addLayout(roster_layout)
roster_layout.addWidget(QLabel("Assigned pilots:"))
roster_layout.addLayout(self.roster_editor)
# When an off-map spawn overrides the start type to in-flight, we save # When an off-map spawn overrides the start type to in-flight, we save
# the selected type into this value. If a non-off-map spawn is selected # the selected type into this value. If a non-off-map spawn is selected
@ -142,6 +151,10 @@ class QFlightCreator(QDialog):
def set_custom_name_text(self, text: str): def set_custom_name_text(self, text: str):
self.custom_name_text = text self.custom_name_text = text
def resize_roster(self, new_size: int) -> None:
self.roster_editor.roster.resize(new_size)
self.roster_editor.resize(new_size)
def verify_form(self) -> Optional[str]: def verify_form(self) -> Optional[str]:
aircraft: Optional[Type[FlyingType]] = self.aircraft_selector.currentData() aircraft: Optional[Type[FlyingType]] = self.aircraft_selector.currentData()
squadron: Optional[Squadron] = self.squadron_selector.currentData() squadron: Optional[Squadron] = self.squadron_selector.currentData()
@ -181,7 +194,7 @@ class QFlightCreator(QDialog):
origin = self.departure.currentData() origin = self.departure.currentData()
arrival = self.arrival.currentData() arrival = self.arrival.currentData()
divert = self.divert.currentData() divert = self.divert.currentData()
size = self.flight_size_spinner.value() roster = self.roster_editor.roster
if arrival is None: if arrival is None:
arrival = origin arrival = origin
@ -190,22 +203,17 @@ class QFlightCreator(QDialog):
self.package, self.package,
self.country, self.country,
squadron, squadron,
size, # A bit of a hack to work around the old API. Not actually relevant because
# the roster is passed explicitly. Needs a refactor.
roster.max_size,
task, task,
self.start_type.currentText(), self.start_type.currentText(),
origin, origin,
arrival, arrival,
divert, divert,
custom_name=self.custom_name_text, custom_name=self.custom_name_text,
roster=roster,
) )
for pilot, idx in zip(flight.pilots, range(self.client_slots_spinner.value())):
if pilot is None:
logging.error(
f"Cannot create client slot because {flight} has no pilot for "
f"aircraft {idx}"
)
continue
pilot.player = True
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
self.created.emit(flight) self.created.emit(flight)
@ -234,14 +242,22 @@ class QFlightCreator(QDialog):
self.start_type.setCurrentText(self.restore_start_type) self.start_type.setCurrentText(self.restore_start_type)
self.restore_start_type = None self.restore_start_type = None
def on_task_changed(self) -> None: def on_task_changed(self, index: int) -> None:
task = self.task_selector.itemData(index)
self.aircraft_selector.update_items( self.aircraft_selector.update_items(
self.task_selector.currentData(), task, self.game.aircraft_inventory.available_types_for_player
self.game.aircraft_inventory.available_types_for_player,
)
self.squadron_selector.update_items(
self.task_selector.currentData(), self.aircraft_selector.currentData()
) )
self.squadron_selector.update_items(task, self.aircraft_selector.currentData())
def on_squadron_changed(self, index: int) -> None:
squadron = self.squadron_selector.itemData(index)
# Clear the roster first so we return the pilots to the pool. This way if we end
# up repopulating from the same squadron we'll get the same pilots back.
self.roster_editor.replace(None)
if squadron is not None:
self.roster_editor.replace(
FlightRoster(squadron, self.flight_size_spinner.value())
)
def update_max_size(self, available: int) -> None: def update_max_size(self, available: int) -> None:
self.flight_size_spinner.setMaximum(min(available, 4)) self.flight_size_spinner.setMaximum(min(available, 4))

View File

@ -28,7 +28,11 @@ class SquadronSelector(QComboBox):
self, task: Optional[FlightType], aircraft: Optional[Type[FlyingType]] self, task: Optional[FlightType], aircraft: Optional[Type[FlyingType]]
) -> None: ) -> None:
current_squadron = self.currentData() current_squadron = self.currentData()
self.clear() self.blockSignals(True)
try:
self.clear()
finally:
self.blockSignals(False)
if task is None: if task is None:
self.addItem("No task selected", None) self.addItem("No task selected", None)
return return

View File

@ -15,16 +15,16 @@ from PySide2.QtWidgets import (
from game import Game from game import Game
from game.squadrons import Pilot from game.squadrons import Pilot
from gen.flights.flight import Flight from gen.flights.flight import Flight, FlightRoster
from qt_ui.models import PackageModel from qt_ui.models import PackageModel
class PilotSelector(QComboBox): class PilotSelector(QComboBox):
available_pilots_changed = Signal() available_pilots_changed = Signal()
def __init__(self, flight: Flight, idx: int) -> None: def __init__(self, roster: Optional[FlightRoster], idx: int) -> None:
super().__init__() super().__init__()
self.flight = flight self.roster = roster
self.pilot_index = idx self.pilot_index = idx
self.rebuild() self.rebuild()
@ -34,15 +34,15 @@ class PilotSelector(QComboBox):
def _do_rebuild(self) -> None: def _do_rebuild(self) -> None:
self.clear() self.clear()
if self.pilot_index >= self.flight.count: if self.roster is None or self.pilot_index >= self.roster.max_size:
self.addItem("No aircraft", None) self.addItem("No aircraft", None)
self.setDisabled(True) self.setDisabled(True)
return return
self.setEnabled(True) self.setEnabled(True)
self.addItem("Unassigned", None) self.addItem("Unassigned", None)
choices = list(self.flight.squadron.available_pilots) choices = list(self.roster.squadron.available_pilots)
current_pilot = self.flight.pilots[self.pilot_index] current_pilot = self.roster.pilots[self.pilot_index]
if current_pilot is not None: if current_pilot is not None:
choices.append(current_pilot) choices.append(current_pilot)
# Put players first, otherwise alphabetically. # Put players first, otherwise alphabetically.
@ -70,19 +70,23 @@ class PilotSelector(QComboBox):
# The roster resize is handled separately, so we have no pilots to remove. # The roster resize is handled separately, so we have no pilots to remove.
return return
pilot = self.itemData(index) pilot = self.itemData(index)
if pilot == self.flight.pilots[self.pilot_index]: if pilot == self.roster.pilots[self.pilot_index]:
return return
self.flight.set_pilot(self.pilot_index, pilot) self.roster.set_pilot(self.pilot_index, pilot)
self.available_pilots_changed.emit() self.available_pilots_changed.emit()
def replace(self, new_roster: Optional[FlightRoster]) -> None:
self.roster = new_roster
self.rebuild()
class PilotControls(QHBoxLayout): class PilotControls(QHBoxLayout):
def __init__(self, flight: Flight, idx: int) -> None: def __init__(self, roster: Optional[FlightRoster], idx: int) -> None:
super().__init__() super().__init__()
self.flight = flight self.roster = roster
self.pilot_index = idx self.pilot_index = idx
self.selector = PilotSelector(flight, idx) self.selector = PilotSelector(roster, idx)
self.selector.currentIndexChanged.connect(self.on_pilot_changed) self.selector.currentIndexChanged.connect(self.on_pilot_changed)
self.addWidget(self.selector) self.addWidget(self.selector)
@ -95,9 +99,9 @@ class PilotControls(QHBoxLayout):
@property @property
def pilot(self) -> Optional[Pilot]: def pilot(self) -> Optional[Pilot]:
if self.pilot_index >= self.flight.count: if self.roster is None or self.pilot_index >= self.roster.max_size:
return None return None
return self.flight.pilots[self.pilot_index] return self.roster.pilots[self.pilot_index]
def on_player_toggled(self, checked: bool) -> None: def on_player_toggled(self, checked: bool) -> None:
pilot = self.pilot pilot = self.pilot
@ -130,12 +134,21 @@ class PilotControls(QHBoxLayout):
finally: finally:
self.player_checkbox.blockSignals(False) self.player_checkbox.blockSignals(False)
def replace(self, new_roster: Optional[FlightRoster]) -> None:
self.roster = new_roster
if self.roster is None or self.pilot_index >= self.roster.max_size:
self.disable_and_clear()
else:
self.enable_and_reset()
self.selector.replace(new_roster)
class FlightRosterEditor(QVBoxLayout): class FlightRosterEditor(QVBoxLayout):
MAX_PILOTS = 4 MAX_PILOTS = 4
def __init__(self, flight: Flight) -> None: def __init__(self, roster: Optional[FlightRoster]) -> None:
super().__init__() super().__init__()
self.roster = roster
self.pilot_controls = [] self.pilot_controls = []
for pilot_idx in range(self.MAX_PILOTS): for pilot_idx in range(self.MAX_PILOTS):
@ -146,7 +159,7 @@ class FlightRosterEditor(QVBoxLayout):
return callback return callback
controls = PilotControls(flight, pilot_idx) controls = PilotControls(roster, pilot_idx)
controls.selector.available_pilots_changed.connect( controls.selector.available_pilots_changed.connect(
make_reset_callback(pilot_idx) make_reset_callback(pilot_idx)
) )
@ -167,6 +180,13 @@ class FlightRosterEditor(QVBoxLayout):
for controls in self.pilot_controls[new_size:]: for controls in self.pilot_controls[new_size:]:
controls.disable_and_clear() controls.disable_and_clear()
def replace(self, new_roster: Optional[FlightRoster]) -> None:
if self.roster is not None:
self.roster.clear()
self.roster = new_roster
for controls in self.pilot_controls:
controls.replace(new_roster)
class QFlightSlotEditor(QGroupBox): class QFlightSlotEditor(QGroupBox):
def __init__(self, package_model: PackageModel, flight: Flight, game: Game): def __init__(self, package_model: PackageModel, flight: Flight, game: Game):
@ -196,7 +216,7 @@ class QFlightSlotEditor(QGroupBox):
layout.addWidget(QLabel(str(self.flight.squadron)), 1, 1) layout.addWidget(QLabel(str(self.flight.squadron)), 1, 1)
layout.addWidget(QLabel("Assigned pilots:"), 2, 0) layout.addWidget(QLabel("Assigned pilots:"), 2, 0)
self.roster_editor = FlightRosterEditor(flight) self.roster_editor = FlightRosterEditor(flight.roster)
layout.addLayout(self.roster_editor, 2, 1) layout.addLayout(self.roster_editor, 2, 1)
self.setLayout(layout) self.setLayout(layout)