diff --git a/changelog.md b/changelog.md
index 581d048b..eb6d4fe8 100644
--- a/changelog.md
+++ b/changelog.md
@@ -5,8 +5,7 @@
* **[Flight Planner]** Added BAI missions.
* **[Flight Planner]** Added anti-ship missions.
* **[Flight Planner]** Differentiated BARCAP and TARCAP. TARCAP is now for hostile areas and will arrive before the package.
-* **[Modding]** Possible to setup liveries overrides for factions
-* **[Units]** Added support for F-14A-135-GR
+* **[Culling]** Added possibility to include/exclude carriers from culling zones
# 2.2.1
@@ -15,13 +14,17 @@
* **[Factions]** Added map Persian Gulf full by Plob
* **[Flight Planner]** Player flights with start delays under ten minutes will spawn immediately.
* **[UI]** Mission start screen now informs players about delayed flights.
+* **[Units]** Added support for F-14A-135-GR
+* **[Modding]** Possible to setup liveries overrides in factions definition files
## Fixes :
* **[Flight Planner]** Hold, join, and split points are planned cautiously near enemy airfields. Ascend/descend points are no longer planned.
-* **[Flight Planner]** Spitfire clipped wing variant was not seen as a flyable module
* **[Flight Planner]** Custom waypoints are usable again. Not that in most cases custom flight plans will revert to the 2.1 flight planning behavior.
* **[Flight Planner]** Fixed UI bug that made it possible to create empty flights which would throw an error.
* **[Flight Planner]** Player flights from carriers will now be delayed correctly according to the player's settings.
+* **[Misc]** Spitfire variant with clipped wings was not seen as flyable by DCS Liberation (hence could not be setup as client/player slot)
+* **[Misc]** Updated Syria terrain parking slots database, the out-of-date database could end up generating aircraft in wrong slots (We are still experiencing issues with somes airbases, such as Khalkhalah though)
+
# 2.2.0
diff --git a/game/db.py b/game/db.py
index c73f7dbf..05eaad06 100644
--- a/game/db.py
+++ b/game/db.py
@@ -1244,7 +1244,7 @@ def unit_type_name_2(unit_type) -> str:
return unit_type.name and unit_type.name or unit_type.id
-def unit_type_from_name(name: str) -> Optional[UnitType]:
+def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
if name in vehicle_map:
return vehicle_map[name]
elif name in plane_map:
diff --git a/game/event/event.py b/game/event/event.py
index ea5f3b80..db8e5ee6 100644
--- a/game/event/event.py
+++ b/game/event/event.py
@@ -107,14 +107,16 @@ class Event:
for destroyed_aircraft in debriefing.killed_aircrafts:
try:
cpid = int(destroyed_aircraft.split("|")[3])
- type = db.unit_type_from_name(destroyed_aircraft.split("|")[4])
- if cpid in cp_map.keys():
+ aircraft = db.unit_type_from_name(
+ destroyed_aircraft.split("|")[4])
+ if cpid in cp_map:
cp = cp_map[cpid]
- if type in cp.base.aircraft.keys():
- logging.info("Aircraft destroyed : " + str(type))
- cp.base.aircraft[type] = max(0, cp.base.aircraft[type]-1)
- except Exception as e:
- print(e)
+ if aircraft in cp.base.aircraft:
+ logging.info(f"Aircraft destroyed: {aircraft}")
+ cp.base.aircraft[aircraft] = max(
+ 0, cp.base.aircraft[aircraft] - 1)
+ except Exception:
+ logging.exception("Failed to commit destroyed aircraft")
# ------------------------------
# Destroyed ground units
@@ -123,13 +125,13 @@ class Event:
for killed_ground_unit in debriefing.killed_ground_units:
try:
cpid = int(killed_ground_unit.split("|")[3])
- type = db.unit_type_from_name(killed_ground_unit.split("|")[4])
+ aircraft = 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 type in cp.base.armor.keys():
- logging.info("Ground unit destroyed : " + str(type))
- cp.base.armor[type] = max(0, cp.base.armor[type] - 1)
+ if aircraft in cp.base.armor.keys():
+ logging.info("Ground unit destroyed : " + str(aircraft))
+ cp.base.armor[aircraft] = max(0, cp.base.armor[aircraft] - 1)
except Exception as e:
print(e)
@@ -352,11 +354,13 @@ class Event:
logging.info(info.text)
-
class UnitsDeliveryEvent(Event):
+
informational = True
- def __init__(self, attacker_name: str, defender_name: str, from_cp: ControlPoint, to_cp: ControlPoint, game):
+ def __init__(self, attacker_name: str, defender_name: str,
+ from_cp: ControlPoint, to_cp: ControlPoint,
+ game: Game) -> None:
super(UnitsDeliveryEvent, self).__init__(game=game,
location=to_cp.position,
from_cp=from_cp,
@@ -364,17 +368,16 @@ class UnitsDeliveryEvent(Event):
attacker_name=attacker_name,
defender_name=defender_name)
- self.units: Dict[UnitType, int] = {}
+ self.units: Dict[Type[UnitType], int] = {}
- def __str__(self):
+ def __str__(self) -> str:
return "Pending delivery to {}".format(self.to_cp)
- def deliver(self, units: Dict[UnitType, int]):
+ def deliver(self, units: Dict[Type[UnitType], int]) -> None:
for k, v in units.items():
self.units[k] = self.units.get(k, 0) + v
- def skip(self):
-
+ def skip(self) -> None:
for k, v in self.units.items():
info = Information("Ally Reinforcement", str(k.id) + " x " + str(v) + " at " + self.to_cp.name, self.game.turn)
self.game.informations.append(info)
diff --git a/game/game.py b/game/game.py
index 3b29c46c..8f691b52 100644
--- a/game/game.py
+++ b/game/game.py
@@ -84,7 +84,8 @@ class Game:
self.ground_planners: Dict[int, GroundPlanner] = {}
self.informations = []
self.informations.append(Information("Game Start", "-" * 40, 0))
- self.__culling_points = self.compute_conflicts_position()
+ self.__culling_points: List[Point] = []
+ self.compute_conflicts_position()
self.__destroyed_units: List[str] = []
self.savepath = ""
self.budget = PLAYER_BUDGET_INITIAL
@@ -239,7 +240,7 @@ class Game:
self.aircraft_inventory.set_from_control_point(cp)
# Plan flights & combat for next turn
- self.__culling_points = self.compute_conflicts_position()
+ self.compute_conflicts_position()
self.ground_planners = {}
self.blue_ato.clear()
self.red_ato.clear()
@@ -316,7 +317,7 @@ class Game:
if i > 50 or budget_for_aircraft <= 0:
break
target_cp = random.choice(potential_cp_armor)
- if target_cp.base.total_planes >= MAX_AIRCRAFT:
+ if target_cp.base.total_aircraft >= MAX_AIRCRAFT:
continue
unit = random.choice(potential_units)
price = db.PRICES[unit] * 2
@@ -364,6 +365,12 @@ class Game:
points.append(front_line.control_point_a.position)
points.append(front_line.control_point_b.position)
+ # If do_not_cull_carrier is enabled, add carriers as culling point
+ if self.settings.perf_do_not_cull_carrier:
+ for cp in self.theater.controlpoints:
+ if cp.is_carrier or cp.is_lha:
+ points.append(cp.position)
+
# If there is no conflict take the center point between the two nearest opposing bases
if len(points) == 0:
cpoint = None
@@ -387,7 +394,7 @@ class Game:
if len(points) == 0:
points.append(Point(0, 0))
- return points
+ self.__culling_points = points
def add_destroyed_units(self, data):
pos = Point(data["x"], data["z"])
diff --git a/game/inventory.py b/game/inventory.py
index 3c92a80f..80adb72b 100644
--- a/game/inventory.py
+++ b/game/inventory.py
@@ -2,7 +2,7 @@
from __future__ import annotations
from collections import defaultdict
-from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING
+from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING, Type
from dcs.unittype import FlyingType
@@ -17,9 +17,9 @@ class ControlPointAircraftInventory:
def __init__(self, control_point: ControlPoint) -> None:
self.control_point = control_point
- self.inventory: Dict[FlyingType, int] = defaultdict(int)
+ self.inventory: Dict[Type[FlyingType], int] = defaultdict(int)
- def add_aircraft(self, aircraft: FlyingType, count: int) -> None:
+ def add_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
"""Adds aircraft to the inventory.
Args:
@@ -28,7 +28,7 @@ class ControlPointAircraftInventory:
"""
self.inventory[aircraft] += count
- def remove_aircraft(self, aircraft: FlyingType, count: int) -> None:
+ def remove_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
"""Removes aircraft from the inventory.
Args:
@@ -47,7 +47,7 @@ class ControlPointAircraftInventory:
)
self.inventory[aircraft] -= count
- def available(self, aircraft: FlyingType) -> int:
+ def available(self, aircraft: Type[FlyingType]) -> int:
"""Returns the number of available aircraft of the given type.
Args:
diff --git a/game/settings.py b/game/settings.py
index ff73b63c..1e54c1b4 100644
--- a/game/settings.py
+++ b/game/settings.py
@@ -39,6 +39,7 @@ class Settings:
# Performance culling
perf_culling: bool = False
perf_culling_distance: int = 100
+ perf_do_not_cull_carrier = True
# LUA Plugins system
plugins: Dict[str, bool] = field(default_factory=dict)
diff --git a/game/theater/base.py b/game/theater/base.py
index 47b3580e..ba8a72f8 100644
--- a/game/theater/base.py
+++ b/game/theater/base.py
@@ -4,9 +4,8 @@ import math
import typing
from typing import Dict, Type
-from dcs.planes import PlaneType
from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task
-from dcs.unittype import UnitType, VehicleType
+from dcs.unittype import FlyingType, UnitType, VehicleType
from dcs.vehicles import AirDefence, Armor
from game import db
@@ -21,20 +20,16 @@ BASE_MIN_STRENGTH = 0
class Base:
- aircraft = {} # type: typing.Dict[PlaneType, int]
- armor = {} # type: typing.Dict[VehicleType, int]
- aa = {} # type: typing.Dict[AirDefence, int]
- strength = 1 # type: float
def __init__(self):
- self.aircraft = {}
- self.armor = {}
- self.aa = {}
+ self.aircraft: Dict[Type[FlyingType], int] = {}
+ self.armor: Dict[VehicleType, int] = {}
+ self.aa: Dict[AirDefence, int] = {}
self.commision_points: Dict[Type, float] = {}
self.strength = 1
@property
- def total_planes(self) -> int:
+ def total_aircraft(self) -> int:
return sum(self.aircraft.values())
@property
@@ -83,7 +78,7 @@ class Base:
logging.info("{} for {} ({}): {}".format(self, for_type, count, result))
return result
- def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[PlaneType, int]:
+ def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[FlyingType, int]:
return self._find_best_unit(self.aircraft, for_type, count)
def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]:
@@ -155,7 +150,7 @@ class Base:
if task:
count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task])
else:
- count = self.total_planes
+ count = self.total_aircraft
count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength))
return min(min(max(count, PLANES_SCRAMBLE_MIN_BASE), int(PLANES_SCRAMBLE_MAX_BASE * multiplier)), count)
@@ -167,18 +162,18 @@ class Base:
# previous logic removed because we always want the full air defense capabilities.
return self.total_aa
- def scramble_sweep(self, multiplier: float) -> typing.Dict[PlaneType, int]:
+ def scramble_sweep(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
def scramble_last_defense(self):
# return as many CAP-capable aircraft as we can since this is the last defense of the base
# (but not more than 20 - that's just nuts)
- return self._find_best_planes(CAP, min(self.total_planes, 20))
+ return self._find_best_planes(CAP, min(self.total_aircraft, 20))
- def scramble_cas(self, multiplier: float) -> typing.Dict[PlaneType, int]:
+ def scramble_cas(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAS, self.scramble_count(multiplier, CAS))
- def scramble_interceptors(self, multiplier: float) -> typing.Dict[PlaneType, int]:
+ def scramble_interceptors(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
def assemble_attack(self) -> typing.Dict[Armor, int]:
diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py
index ca21d463..476f831a 100644
--- a/game/theater/controlpoint.py
+++ b/game/theater/controlpoint.py
@@ -235,7 +235,7 @@ class ControlPoint(MissionTarget):
return result
@property
- def available_aircraft_slots(self):
+ def total_aircraft_parking(self):
"""
:return: The maximum number of aircraft that can be stored in this control point
"""
@@ -376,6 +376,19 @@ class ControlPoint(MissionTarget):
return False
return True
+ @property
+ def expected_aircraft_next_turn(self) -> int:
+ total = self.base.total_aircraft
+ assert self.pending_unit_deliveries
+ for unit_bought in self.pending_unit_deliveries.units:
+ if issubclass(unit_bought, FlyingType):
+ total += self.pending_unit_deliveries.units[unit_bought]
+ return total
+
+ @property
+ def unclaimed_parking(self) -> int:
+ return self.total_aircraft_parking - self.expected_aircraft_next_turn
+
class OffMapSpawn(ControlPoint):
def __init__(self, id: int, name: str, position: Point):
@@ -391,5 +404,5 @@ class OffMapSpawn(ControlPoint):
yield from []
@property
- def available_aircraft_slots(self) -> int:
+ def total_aircraft_parking(self) -> int:
return 1000
diff --git a/gen/flights/flight.py b/gen/flights/flight.py
index 56d9d04a..276a6396 100644
--- a/gen/flights/flight.py
+++ b/gen/flights/flight.py
@@ -2,7 +2,7 @@ from __future__ import annotations
from datetime import timedelta
from enum import Enum
-from typing import Dict, List, Optional, TYPE_CHECKING
+from typing import Dict, List, Optional, TYPE_CHECKING, Type
from dcs.mapping import Point
from dcs.point import MovingPoint, PointAction
@@ -133,8 +133,8 @@ class FlightWaypoint:
class Flight:
- def __init__(self, package: Package, unit_type: FlyingType, count: int,
- flight_type: FlightType, start_type: str,
+ def __init__(self, package: Package, unit_type: Type[FlyingType],
+ count: int, flight_type: FlightType, start_type: str,
departure: ControlPoint, arrival: ControlPoint,
divert: Optional[ControlPoint]) -> None:
self.package = package
diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py
index 50fc5fba..0937f8c9 100644
--- a/qt_ui/widgets/map/QLiberationMap.py
+++ b/qt_ui/widgets/map/QLiberationMap.py
@@ -296,7 +296,7 @@ class QLiberationMap(QGraphicsView):
# Display Culling
if DisplayOptions.culling and self.game.settings.perf_culling:
- self.display_culling()
+ self.display_culling(scene)
for cp in self.game.theater.controlpoints:
diff --git a/qt_ui/windows/basemenu/QBaseMenu2.py b/qt_ui/windows/basemenu/QBaseMenu2.py
index f9f7c159..3740448a 100644
--- a/qt_ui/windows/basemenu/QBaseMenu2.py
+++ b/qt_ui/windows/basemenu/QBaseMenu2.py
@@ -57,7 +57,7 @@ class QBaseMenu2(QDialog):
title = QLabel("" + self.cp.name + "")
title.setAlignment(Qt.AlignLeft | Qt.AlignTop)
title.setProperty("style", "base-title")
- unitsPower = QLabel("{} / {} / Runway : {}".format(self.cp.base.total_planes, self.cp.base.total_armor,
+ unitsPower = QLabel("{} / {} / Runway : {}".format(self.cp.base.total_aircraft, self.cp.base.total_armor,
"Available" if self.cp.has_runway() else "Unavailable"))
self.topLayout.addWidget(title)
self.topLayout.addWidget(unitsPower)
diff --git a/qt_ui/windows/basemenu/QRecruitBehaviour.py b/qt_ui/windows/basemenu/QRecruitBehaviour.py
index 5cb26a81..7f462c57 100644
--- a/qt_ui/windows/basemenu/QRecruitBehaviour.py
+++ b/qt_ui/windows/basemenu/QRecruitBehaviour.py
@@ -1,4 +1,5 @@
import logging
+from typing import Type
from PySide2.QtWidgets import (
QGroupBox,
@@ -106,7 +107,7 @@ class QRecruitBehaviour:
return row + 1
- def _update_count_label(self, unit_type: UnitType):
+ def _update_count_label(self, unit_type: Type[UnitType]):
self.bought_amount_labels[unit_type].setText("{}".format(
unit_type in self.pending_deliveries.units and "{}".format(self.pending_deliveries.units[unit_type]) or "0"
@@ -125,14 +126,7 @@ class QRecruitBehaviour:
child.setText(
QRecruitBehaviour.BUDGET_FORMAT.format(self.budget))
- def buy(self, unit_type):
-
- if self.maximum_units > 0:
- if self.total_units + 1 > self.maximum_units:
- logging.info("Not enough space left !")
- # TODO : display modal warning
- return
-
+ def buy(self, unit_type: Type[UnitType]):
price = db.PRICES[unit_type]
if self.budget >= price:
self.pending_deliveries.deliver({unit_type: 1})
@@ -158,19 +152,6 @@ class QRecruitBehaviour:
self._update_count_label(unit_type)
self.update_available_budget()
- @property
- def total_units(self):
- total = 0
- for unit_type in self.recruitables_types:
- total += self.cp.base.total_units(unit_type)
-
- if self.pending_deliveries:
- for unit_bought in self.pending_deliveries.units:
- if db.unit_task(unit_bought) in self.recruitables_types:
- total += self.pending_deliveries.units[unit_bought]
-
- return total
-
def set_maximum_units(self, maximum_units):
"""
Set the maximum number of units that can be bought
diff --git a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py
index 3e8fef9f..7c159e94 100644
--- a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py
+++ b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py
@@ -1,3 +1,4 @@
+import logging
from typing import Optional, Set
from PySide2.QtCore import Qt
@@ -31,13 +32,13 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
self.existing_units_labels = {}
# Determine maximum number of aircrafts that can be bought
- self.set_maximum_units(self.cp.available_aircraft_slots)
+ self.set_maximum_units(self.cp.total_aircraft_parking)
self.set_recruitable_types([CAP, CAS])
self.bought_amount_labels = {}
self.existing_units_labels = {}
- self.hangar_status = QHangarStatus(self.total_units, self.cp.available_aircraft_slots)
+ self.hangar_status = QHangarStatus(self.cp)
self.init_ui()
@@ -80,8 +81,13 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
self.setLayout(main_layout)
def buy(self, unit_type):
+ if self.maximum_units > 0:
+ if self.cp.unclaimed_parking <= 0:
+ logging.debug(f"No space for additional aircraft at {self.cp}.")
+ return
+
super().buy(unit_type)
- self.hangar_status.update_label(self.total_units, self.cp.available_aircraft_slots)
+ self.hangar_status.update_label()
def sell(self, unit_type: UnitType):
# Don't need to remove aircraft from the inventory if we're canceling
@@ -99,22 +105,26 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
"assigned to a mission?", QMessageBox.Ok)
return
super().sell(unit_type)
- self.hangar_status.update_label(self.total_units, self.cp.available_aircraft_slots)
+ self.hangar_status.update_label()
class QHangarStatus(QHBoxLayout):
- def __init__(self, current_amount: int, max_amount: int):
- super(QHangarStatus, self).__init__()
+ def __init__(self, control_point: ControlPoint) -> None:
+ super().__init__()
+ self.control_point = control_point
+
self.icon = QLabel()
self.icon.setPixmap(ICONS["Hangar"])
self.text = QLabel("")
- self.update_label(current_amount, max_amount)
+ self.update_label()
self.addWidget(self.icon, Qt.AlignLeft)
self.addWidget(self.text, Qt.AlignLeft)
self.addStretch(50)
self.setAlignment(Qt.AlignLeft)
- def update_label(self, current_amount: int, max_amount: int):
- self.text.setText("{}/{}".format(current_amount, max_amount))
+ def update_label(self) -> None:
+ current_amount = self.control_point.expected_aircraft_next_turn
+ max_amount = self.control_point.total_aircraft_parking
+ self.text.setText(f"{current_amount}/{max_amount}")
diff --git a/qt_ui/windows/settings/QSettingsWindow.py b/qt_ui/windows/settings/QSettingsWindow.py
index 486068f5..1506187d 100644
--- a/qt_ui/windows/settings/QSettingsWindow.py
+++ b/qt_ui/windows/settings/QSettingsWindow.py
@@ -279,6 +279,10 @@ class QSettingsWindow(QDialog):
self.culling_distance.setValue(self.game.settings.perf_culling_distance)
self.culling_distance.valueChanged.connect(self.applySettings)
+ self.culling_do_not_cull_carrier = QCheckBox()
+ self.culling_do_not_cull_carrier.setChecked(self.game.settings.perf_do_not_cull_carrier)
+ self.culling_do_not_cull_carrier.toggled.connect(self.applySettings)
+
self.performanceLayout.addWidget(QLabel("Smoke visual effect on frontline"), 0, 0)
self.performanceLayout.addWidget(self.smoke, 0, 1, alignment=Qt.AlignRight)
self.performanceLayout.addWidget(QLabel("SAM starts in RED alert mode"), 1, 0)
@@ -299,6 +303,8 @@ class QSettingsWindow(QDialog):
self.performanceLayout.addWidget(self.culling, 8, 1, alignment=Qt.AlignRight)
self.performanceLayout.addWidget(QLabel("Culling distance (km)"), 9, 0)
self.performanceLayout.addWidget(self.culling_distance, 9, 1, alignment=Qt.AlignRight)
+ self.performanceLayout.addWidget(QLabel("Do not cull carrier's surroundings"), 10, 0)
+ self.performanceLayout.addWidget(self.culling_do_not_cull_carrier, 10, 1, alignment=Qt.AlignRight)
self.generatorLayout.addWidget(self.gameplay)
self.generatorLayout.addWidget(QLabel("Disabling settings below may improve performance, but will impact the overall quality of the experience."))
@@ -366,9 +372,11 @@ class QSettingsWindow(QDialog):
self.game.settings.perf_culling = self.culling.isChecked()
self.game.settings.perf_culling_distance = int(self.culling_distance.value())
+ self.game.settings.perf_do_not_cull_carrier = self.culling_do_not_cull_carrier.isChecked()
self.game.settings.show_red_ato = self.cheat_options.show_red_ato
+ self.game.compute_conflicts_position()
GameUpdateSignal.get_instance().updateGame(self.game)
def onSelectionChanged(self):