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())):