diff --git a/game/debriefing.py b/game/debriefing.py index 114232ec..dd6a53b8 100644 --- a/game/debriefing.py +++ b/game/debriefing.py @@ -12,7 +12,7 @@ from typing import Any, Callable, Dict, List, Type, TYPE_CHECKING from dcs.unittype import FlyingType, UnitType from game import db -from game.theater import Airfield, TheaterGroundObject +from game.theater import Airfield, ControlPoint, TheaterGroundObject from game.unitmap import UnitMap from gen.flights.flight import Flight @@ -38,6 +38,17 @@ class DebriefingDeadAircraftInfo: return self.flight.departure.captured +@dataclass(frozen=True) +class DebriefingDeadFrontLineUnitInfo: + #: The Flight that resulted in the generated unit. + unit_type: Type[UnitType] + control_point: ControlPoint + + @property + def player_unit(self) -> bool: + return self.control_point.captured + + @dataclass(frozen=True) class DebriefingDeadBuildingInfo: #: The ground object this building was present at. @@ -58,7 +69,7 @@ class AirLosses: if loss.flight.departure.captured != player: continue - losses_by_type[loss.flight.unit_type] += loss.flight.count + losses_by_type[loss.flight.unit_type] += 1 return losses_by_type def surviving_flight_members(self, flight: Flight) -> int: @@ -69,6 +80,20 @@ class AirLosses: return flight.count - losses +@dataclass(frozen=True) +class FrontLineLosses: + losses: List[DebriefingDeadFrontLineUnitInfo] + + def by_type(self, player: bool) -> Dict[Type[UnitType], int]: + losses_by_type: Dict[Type[UnitType], int] = defaultdict(int) + for loss in self.losses: + if loss.control_point.captured != player: + continue + + losses_by_type[loss.unit_type] += 1 + return losses_by_type + + @dataclass(frozen=True) class StateData: #: True if the mission ended. If False, the mission exited abnormally. @@ -116,7 +141,8 @@ class Debriefing: self.enemy_country_id = db.country_id_from_name(game.enemy_country) self.air_losses = self.dead_aircraft() - self.dead_units = self.dead_ground_units() + self.front_line_losses = self.dead_front_line_units() + self.dead_units = [] self.damaged_runways = self.find_damaged_runways() self.dead_aaa_groups: List[DebriefingDeadUnitInfo] = [] self.dead_buildings: List[DebriefingDeadBuildingInfo] = [] @@ -174,6 +200,7 @@ class Debriefing: logging.info("Debriefing pre process results :") logging.info("--------------------------------") logging.info(self.air_losses) + logging.info(self.front_line_losses) logging.info(self.player_dead_units_dict) logging.info(self.enemy_dead_units_dict) logging.info(self.player_dead_buildings_dict) @@ -189,27 +216,17 @@ class Debriefing: losses.append(DebriefingDeadAircraftInfo(flight)) return AirLosses(losses) - def dead_ground_units(self) -> List[DebriefingDeadUnitInfo]: + def dead_front_line_units(self) -> FrontLineLosses: losses = [] for unit_name in self.state_data.killed_ground_units: - try: - if isinstance(unit_name, int): - # For some reason the state file will include many raw - # integers in the list of destroyed units. These might be - # from the smoke effects? - continue - if self._is_airfield(unit_name): - continue - country = int(unit_name.split("|")[1]) - unit_type = db.unit_type_from_name(unit_name.split("|")[4]) - if unit_type is None: - logging.error(f"Could not determine type of {unit_name}") - continue - player_unit = country == self.player_country_id - losses.append(DebriefingDeadUnitInfo(player_unit, unit_type)) - except Exception: - logging.exception(f"Failed to process dead unit {unit_name}") - return losses + unit = self.unit_map.front_line_unit(unit_name) + if unit is None: + # Killed "ground units" might also be runways or TGO units, so + # no need to log an error. + continue + losses.append( + DebriefingDeadFrontLineUnitInfo(unit.unit_type, unit.origin)) + return FrontLineLosses(losses) def find_damaged_runways(self) -> List[Airfield]: losses = [] diff --git a/game/event/event.py b/game/event/event.py index d3f5857f..706da279 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging import math +from collections import defaultdict from typing import Dict, List, Optional, TYPE_CHECKING, Type from dcs.mapping import Point @@ -113,15 +114,8 @@ class Event: self._transfer_aircraft(self.game.red_ato, debriefing.air_losses, for_player=False) - def commit(self, debriefing: Debriefing): - - logging.info("Commiting mission results") - - for damaged_runway in debriefing.damaged_runways: - damaged_runway.damage_runway() - - # ------------------------------ - # Destroyed aircrafts + @staticmethod + def commit_air_losses(debriefing: Debriefing) -> None: for loss in debriefing.air_losses.losses: aircraft = loss.flight.unit_type cp = loss.flight.departure @@ -129,29 +123,38 @@ class Event: if available <= 0: logging.error( f"Found killed {aircraft} from {cp} but that airbase has " - f"none available.") + "none available.") continue logging.info(f"{aircraft} destroyed from {cp}") cp.base.aircraft[aircraft] -= 1 - # ------------------------------ - # Destroyed ground units - killed_unit_count_by_cp = {cp.id: 0 for cp in self.game.theater.controlpoints} - cp_map = {cp.id: cp for cp in self.game.theater.controlpoints} - for killed_ground_unit in debriefing.state_data.killed_ground_units: - try: - cpid = int(killed_ground_unit.split("|")[3]) - unit_type = db.unit_type_from_name(killed_ground_unit.split("|")[4]) - if cpid in cp_map.keys(): - killed_unit_count_by_cp[cpid] = killed_unit_count_by_cp[cpid] + 1 - cp = cp_map[cpid] - if unit_type in cp.base.armor.keys(): - logging.info(f"Ground unit destroyed: {unit_type}") - cp.base.armor[unit_type] = max(0, cp.base.armor[unit_type] - 1) - except Exception: - logging.exception( - f"Could not commit lost ground unit {killed_ground_unit}") + @staticmethod + def commit_front_line_losses(debriefing: Debriefing) -> Dict[int, int]: + killed_unit_count_by_cp: Dict[int, int] = defaultdict(int) + for loss in debriefing.front_line_losses.losses: + unit_type = loss.unit_type + control_point = loss.control_point + killed_unit_count_by_cp[control_point.id] += 1 + available = control_point.base.total_units_of_type(unit_type) + if available <= 0: + logging.error( + f"Found killed {unit_type} from {control_point} but that " + "airbase has none available.") + continue + + logging.info(f"{unit_type} destroyed from {control_point}") + control_point.base.armor[unit_type] -= 1 + return killed_unit_count_by_cp + + def commit(self, debriefing: Debriefing): + logging.info("Committing mission results") + + for damaged_runway in debriefing.damaged_runways: + damaged_runway.damage_runway() + + self.commit_air_losses(debriefing) + killed_unit_count_by_cp = self.commit_front_line_losses(debriefing) # ------------------------------ # Static ground objects diff --git a/game/operation/operation.py b/game/operation/operation.py index 07aa1d2b..a63f1f85 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -400,7 +400,8 @@ class Operation: cls.current_mission, conflict, cls.game, player_gp, enemy_gp, - player_cp.stances[enemy_cp.id] + player_cp.stances[enemy_cp.id], + cls.unit_map ) ground_conflict_gen.generate() cls.jtacs.extend(ground_conflict_gen.jtacs) diff --git a/game/unitmap.py b/game/unitmap.py index 6e229f43..9c6e20e6 100644 --- a/game/unitmap.py +++ b/game/unitmap.py @@ -1,16 +1,26 @@ """Maps generated units back to their Liberation types.""" -from typing import Dict, Optional +from dataclasses import dataclass +from typing import Dict, Optional, Type -from dcs.unitgroup import FlyingGroup +from dcs.unitgroup import FlyingGroup, Group +from dcs.unittype import UnitType -from game.theater import Airfield +from game import db +from game.theater import Airfield, ControlPoint from gen.flights.flight import Flight +@dataclass +class FrontLineUnit: + unit_type: Type[UnitType] + origin: ControlPoint + + class UnitMap: def __init__(self) -> None: self.aircraft: Dict[str, Flight] = {} self.airfields: Dict[str, Airfield] = {} + self.front_line_units: Dict[str, FrontLineUnit] = {} def add_aircraft(self, group: FlyingGroup, flight: Flight) -> None: for unit in group.units: @@ -31,3 +41,18 @@ class UnitMap: def airfield(self, name: str) -> Optional[Airfield]: return self.airfields.get(name, None) + + def add_front_line_units(self, group: Group, origin: ControlPoint) -> None: + for unit in group.units: + # The actual name is a String (the pydcs translatable string), which + # doesn't define __eq__. + name = str(unit.name) + if name in self.front_line_units: + raise RuntimeError(f"Duplicate front line unit: {name}") + unit_type = db.unit_type_from_name(unit.type) + if unit_type is None: + raise RuntimeError(f"Unknown unit type: {unit.type}") + self.front_line_units[name] = FrontLineUnit(unit_type, origin) + + def front_line_unit(self, name: str) -> Optional[FrontLineUnit]: + return self.front_line_units.get(name, None) diff --git a/gen/armor.py b/gen/armor.py index 147576ba..39c5e5e4 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -28,6 +28,7 @@ from dcs.unittype import VehicleType from dcs.unitgroup import VehicleGroup from game import db +from game.unitmap import UnitMap from .naming import namegen from gen.ground_forces.ai_ground_planner import ( CombatGroup, CombatGroupRole, @@ -72,14 +73,14 @@ class JtacInfo: class GroundConflictGenerator: def __init__( - self, - mission: Mission, - conflict: Conflict, - game: Game, - player_planned_combat_groups: List[CombatGroup], - enemy_planned_combat_groups: List[CombatGroup], - player_stance: CombatStance - ): + self, + mission: Mission, + conflict: Conflict, + game: Game, + player_planned_combat_groups: List[CombatGroup], + enemy_planned_combat_groups: List[CombatGroup], + player_stance: CombatStance, + unit_map: UnitMap) -> None: self.mission = mission self.conflict = conflict self.enemy_planned_combat_groups = enemy_planned_combat_groups @@ -87,6 +88,7 @@ class GroundConflictGenerator: self.player_stance = CombatStance(player_stance) self.enemy_stance = self._enemy_stance() self.game = game + self.unit_map = unit_map self.jtacs: List[JtacInfo] = [] def _enemy_stance(self): @@ -530,6 +532,8 @@ class GroundConflictGenerator: heading=heading, move_formation=move_formation) + self.unit_map.add_front_line_units(group, cp) + for c in range(count): vehicle: Vehicle = group.units[c] vehicle.player_can_drive = True diff --git a/qt_ui/windows/QDebriefingWindow.py b/qt_ui/windows/QDebriefingWindow.py index 5dc826f1..4b52d585 100644 --- a/qt_ui/windows/QDebriefingWindow.py +++ b/qt_ui/windows/QDebriefingWindow.py @@ -68,7 +68,10 @@ class QDebriefingWindow(QDialog): logging.exception( f"Issue adding {unit_type} to debriefing information") - for unit_type, count in self.debriefing.player_dead_units_dict.items(): + front_line_losses = self.debriefing.front_line_losses.by_type( + player=True + ) + for unit_type, count in front_line_losses.items(): try: lostUnitsLayout.addWidget( QLabel(db.unit_type_name(unit_type)), row, 0) @@ -111,7 +114,10 @@ class QDebriefingWindow(QDialog): logging.exception( f"Issue adding {unit_type} to debriefing information") - for unit_type, count in self.debriefing.enemy_dead_units_dict.items(): + front_line_losses = self.debriefing.front_line_losses.by_type( + player=False + ) + for unit_type, count in front_line_losses.items(): if count == 0: continue enemylostUnitsLayout.addWidget(QLabel(db.unit_type_name(unit_type)), row, 0) diff --git a/qt_ui/windows/QWaitingForMissionResultWindow.py b/qt_ui/windows/QWaitingForMissionResultWindow.py index c26db853..f780091c 100644 --- a/qt_ui/windows/QWaitingForMissionResultWindow.py +++ b/qt_ui/windows/QWaitingForMissionResultWindow.py @@ -132,16 +132,21 @@ class QWaitingForMissionResultWindow(QDialog): self.debriefing = debriefing updateLayout.addWidget(QLabel("Aircraft destroyed"), 0, 0) - updateLayout.addWidget(QLabel(str(len(debriefing.air_losses.losses))), 0, 1) + updateLayout.addWidget( + QLabel(str(len(debriefing.air_losses.losses))), 0, 1) - updateLayout.addWidget(QLabel("Ground units destroyed"), 1, 0) - updateLayout.addWidget(QLabel(str(len(debriefing.dead_units))), 1, 1) + updateLayout.addWidget( + QLabel("Front line units destroyed"), 1, 0) + updateLayout.addWidget( + QLabel(str(len(debriefing.front_line_losses.losses))), 1, 1) - #updateLayout.addWidget(QLabel("Weapons fired"), 2, 0) - #updateLayout.addWidget(QLabel(str(len(debriefing.weapons_fired))), 2, 1) + updateLayout.addWidget( + QLabel("Other ground units destroyed"), 2, 0) + updateLayout.addWidget(QLabel(str(len(debriefing.dead_units))), 2, 1) - updateLayout.addWidget(QLabel("Base Capture Events"), 2, 0) - updateLayout.addWidget(QLabel(str(len(debriefing.base_capture_events))), 2, 1) + updateLayout.addWidget(QLabel("Base Capture Events"), 3, 0) + updateLayout.addWidget( + QLabel(str(len(debriefing.base_capture_events))), 3, 1) # Clear previous content of the window for i in reversed(range(self.gridLayout.count())):