Merge branch 'develop' into helipads

# Conflicts:
#	game/game.py
#	game/operation/operation.py
#	game/theater/conflicttheater.py
#	game/theater/controlpoint.py
#	gen/groundobjectsgen.py
#	resources/campaigns/golan_heights_lite.miz
This commit is contained in:
Khopa
2021-08-02 19:34:05 +02:00
408 changed files with 9630 additions and 5172 deletions

View File

@@ -112,7 +112,7 @@ def replace_mission_scripting_file():
)
liberation_scripting_path = "./resources/scripts/MissionScripting.lua"
backup_scripting_path = "./resources/scripts/MissionScripting.original.lua"
if os.path.isfile(mission_scripting_path):
if install_dir != "" and os.path.isfile(mission_scripting_path):
with open(mission_scripting_path, "r") as ms:
current_file_content = ms.read()
with open(liberation_scripting_path, "r") as libe_ms:
@@ -133,5 +133,9 @@ def restore_original_mission_scripting():
)
backup_scripting_path = "./resources/scripts/MissionScripting.original.lua"
if os.path.isfile(backup_scripting_path) and os.path.isfile(mission_scripting_path):
if (
install_dir != ""
and os.path.isfile(backup_scripting_path)
and os.path.isfile(mission_scripting_path)
):
copyfile(backup_scripting_path, mission_scripting_path)

View File

@@ -3,6 +3,8 @@ import logging
import os
from logging.handlers import RotatingFileHandler
from qt_ui.logging_handler import HookableInMemoryHandler
def init_logging(version: str) -> None:
"""Initializes the logging configuration."""
@@ -10,13 +12,22 @@ def init_logging(version: str) -> None:
os.mkdir("logs")
fmt = "%(asctime)s :: %(levelname)s :: %(message)s"
formatter = logging.Formatter(fmt)
logging.basicConfig(level=logging.DEBUG, format=fmt)
logger = logging.getLogger()
handler = RotatingFileHandler("./logs/liberation.log", "a", 5000000, 1)
handler.setLevel(logging.DEBUG)
handler.setFormatter(logging.Formatter(fmt))
rotating_file_handler = RotatingFileHandler(
"./logs/liberation.log", "a", 5000000, 1
)
rotating_file_handler.setLevel(logging.DEBUG)
rotating_file_handler.setFormatter(formatter)
logger.addHandler(handler)
hookable_in_memory_handler = HookableInMemoryHandler()
hookable_in_memory_handler.setLevel(logging.DEBUG)
hookable_in_memory_handler.setFormatter(formatter)
logger.addHandler(rotating_file_handler)
logger.addHandler(hookable_in_memory_handler)
logger.info(f"DCS Liberation {version}")

38
qt_ui/logging_handler.py Normal file
View File

@@ -0,0 +1,38 @@
import logging
import typing
LogHook = typing.Callable[[str], None]
class HookableInMemoryHandler(logging.Handler):
"""Hookable in-memory logging handler for logs window"""
_log: str
_hook: typing.Optional[typing.Callable[[str], None]]
def __init__(self, *args, **kwargs):
super(HookableInMemoryHandler, self).__init__(*args, **kwargs)
self._log = ""
self._hook = None
@property
def log(self) -> str:
return self._log
def emit(self, record):
msg = self.format(record)
self._log += msg + "\n"
if self._hook is not None:
self._hook(msg)
def write(self, m):
pass
def clearLog(self) -> None:
self._log = ""
def setHook(self, hook: typing.Callable[[str], None]) -> None:
self._hook = hook
def clearHook(self) -> None:
self._hook = None

View File

@@ -7,18 +7,15 @@ from pathlib import Path
from typing import Optional
from PySide2 import QtWidgets
from PySide2.QtCore import Qt
from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import QApplication, QSplashScreen
from dcs.payloads import PayloadDirectories
from dcs.weapons_data import weapon_ids
from game import Game, VERSION, persistency
from game.data.weapons import (
WEAPON_FALLBACK_MAP,
WEAPON_INTRODUCTION_YEARS,
Weapon,
)
from game.data.weapons import WeaponGroup, Pylon, Weapon
from game.db import FACTIONS
from game.dcs.aircrafttype import AircraftType
from game.profiling import logged_duration
from game.settings import Settings
from game.theater.start_generator import GameGenerator, GeneratorSettings, ModSettings
@@ -62,6 +59,8 @@ def run_ui(game: Optional[Game]) -> None:
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" # Potential fix for 4K screens
app = QApplication(sys.argv)
app.setAttribute(Qt.AA_DisableWindowContextHelpButton)
# init the theme and load the stylesheet based on the theme index
liberation_theme.init()
with open(
@@ -97,6 +96,22 @@ def run_ui(game: Optional[Game]) -> None:
uiconstants.load_aircraft_banners()
uiconstants.load_vehicle_banners()
# Show warning if no DCS Installation directory was set
if liberation_install.get_dcs_install_directory() == "":
QtWidgets.QMessageBox.warning(
splash,
"No DCS installation directory.",
"The DCS Installation directory is not set correctly. "
"This will prevent DCS Liberation to work properly as the MissionScripting "
"file will not be modified."
"<br/><br/>To solve this problem, you can set the Installation directory "
"within the preferences menu. You can also manually edit or replace the "
"following file:"
"<br/><br/><strong>&lt;dcs_installation_directory&gt;/Scripts/MissionScripting.lua</strong>"
"<br/><br/>The easiest way to do it is to replace the original file with the file in dcs-liberation distribution (&lt;dcs_liberation_installation&gt;/resources/scripts/MissionScripting.lua)."
"<br/><br/>You can find more information on how to manually change this file in the Liberation Wiki (Page: Dedicated Server Guide) on GitHub.</p>",
QtWidgets.QMessageBox.StandardButton.Ok,
)
# Replace DCS Mission scripting file to allow DCS Liberation to work
try:
liberation_install.replace_mission_scripting_file()
@@ -169,8 +184,24 @@ def parse_args() -> argparse.Namespace:
"--inverted", action="store_true", help="Invert the campaign."
)
new_game.add_argument(
"--date",
type=datetime.fromisoformat,
default=datetime.today(),
help="Start date of the campaign.",
)
new_game.add_argument(
"--restrict-weapons-by-date",
action="store_true",
help="Enable campaign date restricted weapons.",
)
new_game.add_argument("--cheats", action="store_true", help="Enable cheats.")
lint_weapons = subparsers.add_parser("lint-weapons")
lint_weapons.add_argument("aircraft", help="Name of the aircraft variant to lint.")
return parser.parse_args()
@@ -182,6 +213,8 @@ def create_game(
auto_procurement: bool,
inverted: bool,
cheats: bool,
start_date: datetime,
restrict_weapons_by_date: bool,
) -> Game:
first_start = liberation_install.init()
if first_start:
@@ -210,9 +243,10 @@ def create_game(
automate_aircraft_reinforcements=auto_procurement,
enable_frontline_cheats=cheats,
enable_base_capture_cheat=cheats,
restrict_weapons_by_date=restrict_weapons_by_date,
),
GeneratorSettings(
start_date=datetime.today(),
start_date=start_date,
player_budget=DEFAULT_BUDGET,
enemy_budget=DEFAULT_BUDGET,
midgame=False,
@@ -232,16 +266,24 @@ def create_game(
high_digit_sams=False,
),
)
return generator.generate()
game = generator.generate()
game.begin_turn_0()
return game
def lint_weapon_data() -> None:
for clsid in weapon_ids:
weapon = Weapon.from_clsid(clsid)
if weapon not in WEAPON_INTRODUCTION_YEARS:
logging.warning(f"{weapon} has no introduction date")
if weapon not in WEAPON_FALLBACK_MAP:
logging.warning(f"{weapon} has no fallback")
def lint_all_weapon_data() -> None:
for weapon in WeaponGroup.named("Unknown").weapons:
logging.warning(f"No weapon data for {weapon}: {weapon.clsid}")
def lint_weapon_data_for_aircraft(aircraft: AircraftType) -> None:
all_weapons: set[Weapon] = set()
for pylon in Pylon.iter_pylons(aircraft):
all_weapons |= pylon.allowed
for weapon in all_weapons:
if weapon.weapon_group.name == "Unknown":
logging.warning(f'{weapon.clsid} "{weapon.name}" has no weapon data')
def main():
@@ -255,7 +297,7 @@ def main():
# TODO: Flesh out data and then make unconditional.
if args.warn_missing_weapon_data:
lint_weapon_data()
lint_all_weapon_data()
if args.subcommand == "new-game":
with logged_duration("New game creation"):
@@ -267,7 +309,12 @@ def main():
args.auto_procurement,
args.inverted,
args.cheats,
args.date,
args.restrict_weapons_by_date,
)
if args.subcommand == "lint-weapons":
lint_weapon_data_for_aircraft(AircraftType.named(args.aircraft))
return
run_ui(game)

View File

@@ -12,11 +12,10 @@ from PySide2.QtCore import (
)
from PySide2.QtGui import QIcon
from game import db
from game.game import Game
from game.squadrons import Squadron, Pilot
from game.theater.missiontarget import MissionTarget
from game.transfers import TransferOrder
from game.transfers import TransferOrder, PendingTransfers
from gen.ato import AirTaskingOrder, Package
from gen.flights.flight import Flight, FlightType
from gen.flights.traveltime import TotEstimator
@@ -281,9 +280,9 @@ class AtoModel(QAbstractListModel):
self.package_models.clear()
if self.game is not None:
if player:
self.ato = self.game.blue_ato
self.ato = self.game.blue.ato
else:
self.ato = self.game.red_ato
self.ato = self.game.red.ato
else:
self.ato = AirTaskingOrder()
self.endResetModel()
@@ -316,8 +315,12 @@ class TransferModel(QAbstractListModel):
super().__init__()
self.game_model = game_model
@property
def transfers(self) -> PendingTransfers:
return self.game_model.game.coalition_for(player=True).transfers
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
return self.game_model.game.transfers.pending_transfer_count
return self.transfers.pending_transfer_count
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
if not index.isValid():
@@ -345,7 +348,7 @@ class TransferModel(QAbstractListModel):
"""Updates the game with the new unit transfer."""
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
# TODO: Needs to regenerate base inventory tab.
self.game_model.game.transfers.new_transfer(transfer)
self.transfers.new_transfer(transfer)
self.endInsertRows()
def cancel_transfer_at_index(self, index: QModelIndex) -> None:
@@ -354,15 +357,15 @@ class TransferModel(QAbstractListModel):
def cancel_transfer(self, transfer: TransferOrder) -> None:
"""Cancels the planned unit transfer at the given index."""
index = self.game_model.game.transfers.index_of_transfer(transfer)
index = self.transfers.index_of_transfer(transfer)
self.beginRemoveRows(QModelIndex(), index, index)
# TODO: Needs to regenerate base inventory tab.
self.game_model.game.transfers.cancel_transfer(transfer)
self.transfers.cancel_transfer(transfer)
self.endRemoveRows()
def transfer_at_index(self, index: QModelIndex) -> TransferOrder:
"""Returns the transfer located at the given index."""
return self.game_model.game.transfers.transfer_at_index(index.row())
return self.transfers.transfer_at_index(index.row())
class AirWingModel(QAbstractListModel):
@@ -488,8 +491,8 @@ class GameModel:
self.ato_model = AtoModel(self, AirTaskingOrder())
self.red_ato_model = AtoModel(self, AirTaskingOrder())
else:
self.ato_model = AtoModel(self, self.game.blue_ato)
self.red_ato_model = AtoModel(self, self.game.red_ato)
self.ato_model = AtoModel(self, self.game.blue.ato)
self.red_ato_model = AtoModel(self, self.game.red.ato)
def ato_model_for(self, player: bool) -> AtoModel:
if player:

View File

@@ -68,6 +68,7 @@ def load_icons():
ICONS["Terrain_Normandy"] = QPixmap("./resources/ui/terrain_normandy.gif")
ICONS["Terrain_TheChannel"] = QPixmap("./resources/ui/terrain_channel.gif")
ICONS["Terrain_Syria"] = QPixmap("./resources/ui/terrain_syria.gif")
ICONS["Terrain_MarianaIslands"] = QPixmap("./resources/ui/terrain_marianas.gif")
ICONS["Dawn"] = QPixmap("./resources/ui/conditions/timeofday/dawn.png")
ICONS["Day"] = QPixmap("./resources/ui/conditions/timeofday/day.png")
@@ -106,6 +107,7 @@ def load_icons():
ICONS["PluginsOptions"] = QPixmap(
"./resources/ui/misc/" + get_theme_icons() + "/pluginsoptions.png"
)
ICONS["Notes"] = QPixmap("./resources/ui/misc/" + get_theme_icons() + "/notes.png")
ICONS["TaskCAS"] = QPixmap("./resources/ui/tasks/cas.png")
ICONS["TaskCAP"] = QPixmap("./resources/ui/tasks/cap.png")

View File

@@ -1,6 +1,7 @@
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QPushButton
import qt_ui.uiconstants as CONST
from game import Game
from game.income import Income
from qt_ui.windows.finances.QFinancesMenu import QFinancesMenu
@@ -10,7 +11,7 @@ class QBudgetBox(QGroupBox):
UI Component to display current budget and player's money
"""
def __init__(self, game):
def __init__(self, game: Game):
super(QBudgetBox, self).__init__("Budget")
self.game = game
@@ -40,7 +41,7 @@ class QBudgetBox(QGroupBox):
return
self.game = game
self.setBudget(self.game.budget, Income(self.game, player=True).total)
self.setBudget(self.game.blue.budget, Income(self.game, player=True).total)
self.finances.setEnabled(True)
def openFinances(self):

View File

@@ -24,8 +24,8 @@ class QFactionsInfos(QGroupBox):
def setGame(self, game: Game):
if game is not None:
self.player_name.setText(game.player_faction.name)
self.enemy_name.setText(game.enemy_faction.name)
self.player_name.setText(game.blue.faction.name)
self.enemy_name.setText(game.red.faction.name)
else:
self.player_name.setText("")
self.enemy_name.setText("")

View File

@@ -13,6 +13,7 @@ import qt_ui.uiconstants as CONST
from game import Game
from game.event.airwar import AirWarEvent
from game.profiling import logged_duration
from game.utils import meters
from gen.ato import Package
from gen.flights.traveltime import TotEstimator
from qt_ui.models import GameModel
@@ -112,6 +113,14 @@ class QTopPanel(QFrame):
self.transfers.setEnabled(True)
self.conditionsWidget.setCurrentTurn(game.turn, game.conditions)
if game.conditions.weather.clouds:
base_m = game.conditions.weather.clouds.base
base_ft = int(meters(base_m).feet)
self.conditionsWidget.setToolTip(f"Cloud Base: {base_m}m / {base_ft}ft")
else:
self.conditionsWidget.setToolTip("")
self.intel_box.set_game(game)
self.budgetBox.setGame(game)
self.factionsInfos.setGame(game)
@@ -159,7 +168,7 @@ class QTopPanel(QFrame):
package.time_over_target = estimator.earliest_tot()
def ato_has_clients(self) -> bool:
for package in self.game.blue_ato.packages:
for package in self.game.blue.ato.packages:
for flight in package.flights:
if flight.client_count > 0:
return True
@@ -227,7 +236,7 @@ class QTopPanel(QFrame):
def check_no_missing_pilots(self) -> bool:
missing_pilots = []
for package in self.game.blue_ato.packages:
for package in self.game.blue.ato.packages:
for flight in package.flights:
if flight.missing_pilots > 0:
missing_pilots.append((package, flight))
@@ -273,8 +282,8 @@ class QTopPanel(QFrame):
closest_cps[0],
closest_cps[1],
self.game.theater.controlpoints[0].position,
self.game.player_faction.name,
self.game.enemy_faction.name,
self.game.blue.faction.name,
self.game.red.faction.name,
)
unit_map = self.game.initiate_event(game_event)

View File

@@ -4,7 +4,6 @@ from typing import Iterable, Type
from PySide2.QtWidgets import QComboBox
from dcs.unittype import FlyingType
from game import db
from gen.flights.ai_flight_planner_db import aircraft_for_task
from gen.flights.flight import FlightType
@@ -13,16 +12,12 @@ class QAircraftTypeSelector(QComboBox):
"""Combo box for selecting among the given aircraft types."""
def __init__(
self,
aircraft_types: Iterable[Type[FlyingType]],
country: str,
mission_type: FlightType,
self, aircraft_types: Iterable[Type[FlyingType]], mission_type: FlightType
) -> None:
super().__init__()
self.model().sort(0)
self.setSizeAdjustPolicy(self.AdjustToContents)
self.country = country
self.update_items(mission_type, aircraft_types)
def update_items(self, mission_type: FlightType, aircraft_types):

View File

@@ -8,11 +8,18 @@ from PySide2.QtCore import Property, QObject, Signal, Slot
from dcs import Point
from dcs.unit import Unit
from dcs.vehicles import vehicle_map
from shapely.geometry import LineString, Point as ShapelyPoint, Polygon, MultiPolygon
from shapely.geometry import (
LineString,
Point as ShapelyPoint,
Polygon,
MultiPolygon,
MultiLineString,
)
from game import Game
from game.dcs.groundunittype import GroundUnitType
from game.navmesh import NavMesh
from game.flightplan import JoinZoneGeometry, HoldZoneGeometry
from game.navmesh import NavMesh, NavMeshPoly
from game.profiling import logged_duration
from game.theater import (
ConflictTheater,
@@ -27,7 +34,12 @@ from game.transfers import MultiGroupTransport, TransportMap
from game.utils import meters, nautical_miles
from gen.ato import AirTaskingOrder
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
from gen.flights.flightplan import FlightPlan, PatrollingFlightPlan, CasFlightPlan
from gen.flights.flightplan import (
FlightPlan,
PatrollingFlightPlan,
CasFlightPlan,
)
from game.flightplan.ipzonegeometry import IpZoneGeometry
from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel, AtoModel
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
@@ -39,6 +51,10 @@ LeafletPoly = list[LeafletLatLon]
MAX_SHIP_DISTANCE = nautical_miles(80)
# Set to True to enable computing expensive debugging information. At the time of
# writing this only controls computing the waypoint placement zones.
ENABLE_EXPENSIVE_DEBUG_TOOLS = False
# **EVERY PROPERTY NEEDS A NOTIFY SIGNAL**
#
# https://bugreports.qt.io/browse/PYSIDE-1426
@@ -73,6 +89,18 @@ def shapely_to_leaflet_polys(
return [shapely_poly_to_leaflet_points(poly, theater) for poly in polys]
def shapely_line_to_leaflet_points(
line: LineString, theater: ConflictTheater
) -> list[LeafletLatLon]:
return [theater.point_to_ll(Point(x, y)).as_list() for x, y in line.coords]
def shapely_lines_to_leaflet_points(
lines: MultiLineString, theater: ConflictTheater
) -> list[list[LeafletLatLon]]:
return [shapely_line_to_leaflet_points(l, theater) for l in lines.geoms]
class ControlPointJs(QObject):
nameChanged = Signal()
blueChanged = Signal()
@@ -336,8 +364,12 @@ class SupplyRouteJs(QObject):
def find_transports(self) -> List[MultiGroupTransport]:
if self.sea_route:
return self.find_in_transport_map(self.game.transfers.cargo_ships)
return self.find_in_transport_map(self.game.transfers.convoys)
return self.find_in_transport_map(
self.game.blue.transfers.cargo_ships
) + self.find_in_transport_map(self.game.red.transfers.cargo_ships)
return self.find_in_transport_map(
self.game.blue.transfers.convoys
) + self.find_in_transport_map(self.game.red.transfers.convoys)
@Property(list, notify=activeTransportsChanged)
def activeTransports(self) -> List[str]:
@@ -385,12 +417,12 @@ class FrontLineJs(QObject):
def extents(self) -> List[LeafletLatLon]:
a = self.theater.point_to_ll(
self.front_line.position.point_from_heading(
self.front_line.attack_heading + 90, nautical_miles(2).meters
self.front_line.attack_heading.right.degrees, nautical_miles(2).meters
)
)
b = self.theater.point_to_ll(
self.front_line.position.point_from_heading(
self.front_line.attack_heading + 270, nautical_miles(2).meters
self.front_line.attack_heading.left.degrees, nautical_miles(2).meters
)
)
return [[a.latitude, a.longitude], [b.latitude, b.longitude]]
@@ -508,6 +540,19 @@ class FlightJs(QObject):
selectedChanged = Signal()
commitBoundaryChanged = Signal()
originChanged = Signal()
@Property(list, notify=originChanged)
def origin(self) -> LeafletLatLon:
return self._waypoints[0].position
targetChanged = Signal()
@Property(list, notify=targetChanged)
def target(self) -> LeafletLatLon:
ll = self.theater.point_to_ll(self.flight.package.target.position)
return [ll.latitude, ll.longitude]
def __init__(
self,
flight: Flight,
@@ -642,11 +687,35 @@ class ThreatZoneContainerJs(QObject):
return self._red
class NavMeshPolyJs(QObject):
polyChanged = Signal()
threatenedChanged = Signal()
def __init__(self, poly: LeafletPoly, threatened: bool) -> None:
super().__init__()
self._poly = poly
self._threatened = threatened
@Property(list, notify=polyChanged)
def poly(self) -> LeafletPoly:
return self._poly
@Property(bool, notify=threatenedChanged)
def threatened(self) -> bool:
return self._threatened
@classmethod
def from_navmesh(cls, poly: NavMeshPoly, theater: ConflictTheater) -> NavMeshPolyJs:
return NavMeshPolyJs(
shapely_poly_to_leaflet_points(poly.poly, theater), poly.threatened
)
class NavMeshJs(QObject):
blueChanged = Signal()
redChanged = Signal()
def __init__(self, blue: list[LeafletPoly], red: list[LeafletPoly]) -> None:
def __init__(self, blue: list[NavMeshPolyJs], red: list[NavMeshPolyJs]) -> None:
super().__init__()
self._blue = blue
self._red = red
@@ -663,17 +732,17 @@ class NavMeshJs(QObject):
return self._red
@staticmethod
def to_polys(navmesh: NavMesh, theater: ConflictTheater) -> list[LeafletPoly]:
def to_polys(navmesh: NavMesh, theater: ConflictTheater) -> list[NavMeshPolyJs]:
polys = []
for poly in navmesh.polys:
polys.append(shapely_poly_to_leaflet_points(poly.poly, theater))
polys.append(NavMeshPolyJs.from_navmesh(poly, theater))
return polys
@classmethod
def from_game(cls, game: Game) -> NavMeshJs:
return NavMeshJs(
cls.to_polys(game.blue_navmesh, game.theater),
cls.to_polys(game.red_navmesh, game.theater),
cls.to_polys(game.blue.nav_mesh, game.theater),
cls.to_polys(game.red.nav_mesh, game.theater),
)
@@ -741,6 +810,209 @@ class UnculledZone(QObject):
)
class IpZonesJs(QObject):
homeBubbleChanged = Signal()
ipBubbleChanged = Signal()
permissibleZoneChanged = Signal()
safeZonesChanged = Signal()
def __init__(
self,
home_bubble: LeafletPoly,
ip_bubble: LeafletPoly,
permissible_zone: LeafletPoly,
safe_zones: list[LeafletPoly],
) -> None:
super().__init__()
self._home_bubble = home_bubble
self._ip_bubble = ip_bubble
self._permissible_zone = permissible_zone
self._safe_zones = safe_zones
@Property(list, notify=homeBubbleChanged)
def homeBubble(self) -> LeafletPoly:
return self._home_bubble
@Property(list, notify=ipBubbleChanged)
def ipBubble(self) -> LeafletPoly:
return self._ip_bubble
@Property(list, notify=permissibleZoneChanged)
def permissibleZone(self) -> LeafletPoly:
return self._permissible_zone
@Property(list, notify=safeZonesChanged)
def safeZones(self) -> list[LeafletPoly]:
return self._safe_zones
@classmethod
def empty(cls) -> IpZonesJs:
return IpZonesJs([], [], [], [])
@classmethod
def for_flight(cls, flight: Flight, game: Game) -> IpZonesJs:
if not ENABLE_EXPENSIVE_DEBUG_TOOLS:
return IpZonesJs.empty()
target = flight.package.target
home = flight.departure
geometry = IpZoneGeometry(target.position, home.position, game.blue)
return IpZonesJs(
shapely_poly_to_leaflet_points(geometry.home_bubble, game.theater),
shapely_poly_to_leaflet_points(geometry.ip_bubble, game.theater),
shapely_poly_to_leaflet_points(geometry.permissible_zone, game.theater),
shapely_to_leaflet_polys(geometry.safe_zones, game.theater),
)
class JoinZonesJs(QObject):
homeBubbleChanged = Signal()
targetBubbleChanged = Signal()
ipBubbleChanged = Signal()
excludedZonesChanged = Signal()
permissibleZonesChanged = Signal()
preferredLinesChanged = Signal()
def __init__(
self,
home_bubble: LeafletPoly,
target_bubble: LeafletPoly,
ip_bubble: LeafletPoly,
excluded_zones: list[LeafletPoly],
permissible_zones: list[LeafletPoly],
preferred_lines: list[list[LeafletLatLon]],
) -> None:
super().__init__()
self._home_bubble = home_bubble
self._target_bubble = target_bubble
self._ip_bubble = ip_bubble
self._excluded_zones = excluded_zones
self._permissible_zones = permissible_zones
self._preferred_lines = preferred_lines
@Property(list, notify=homeBubbleChanged)
def homeBubble(self) -> LeafletPoly:
return self._home_bubble
@Property(list, notify=targetBubbleChanged)
def targetBubble(self) -> LeafletPoly:
return self._target_bubble
@Property(list, notify=ipBubbleChanged)
def ipBubble(self) -> LeafletPoly:
return self._ip_bubble
@Property(list, notify=excludedZonesChanged)
def excludedZones(self) -> list[LeafletPoly]:
return self._excluded_zones
@Property(list, notify=permissibleZonesChanged)
def permissibleZones(self) -> list[LeafletPoly]:
return self._permissible_zones
@Property(list, notify=preferredLinesChanged)
def preferredLines(self) -> list[list[LeafletLatLon]]:
return self._preferred_lines
@classmethod
def empty(cls) -> JoinZonesJs:
return JoinZonesJs([], [], [], [], [], [])
@classmethod
def for_flight(cls, flight: Flight, game: Game) -> JoinZonesJs:
if not ENABLE_EXPENSIVE_DEBUG_TOOLS:
return JoinZonesJs.empty()
target = flight.package.target
home = flight.departure
if flight.package.waypoints is None:
return JoinZonesJs.empty()
ip = flight.package.waypoints.ingress
geometry = JoinZoneGeometry(target.position, home.position, ip, game.blue)
return JoinZonesJs(
shapely_poly_to_leaflet_points(geometry.home_bubble, game.theater),
shapely_poly_to_leaflet_points(geometry.target_bubble, game.theater),
shapely_poly_to_leaflet_points(geometry.ip_bubble, game.theater),
shapely_to_leaflet_polys(geometry.excluded_zones, game.theater),
shapely_to_leaflet_polys(geometry.permissible_zones, game.theater),
shapely_lines_to_leaflet_points(geometry.preferred_lines, game.theater),
)
class HoldZonesJs(QObject):
homeBubbleChanged = Signal()
targetBubbleChanged = Signal()
joinBubbleChanged = Signal()
excludedZonesChanged = Signal()
permissibleZonesChanged = Signal()
preferredLinesChanged = Signal()
def __init__(
self,
home_bubble: LeafletPoly,
target_bubble: LeafletPoly,
join_bubble: LeafletPoly,
excluded_zones: list[LeafletPoly],
permissible_zones: list[LeafletPoly],
preferred_lines: list[list[LeafletLatLon]],
) -> None:
super().__init__()
self._home_bubble = home_bubble
self._target_bubble = target_bubble
self._join_bubble = join_bubble
self._excluded_zones = excluded_zones
self._permissible_zones = permissible_zones
self._preferred_lines = preferred_lines
@Property(list, notify=homeBubbleChanged)
def homeBubble(self) -> LeafletPoly:
return self._home_bubble
@Property(list, notify=targetBubbleChanged)
def targetBubble(self) -> LeafletPoly:
return self._target_bubble
@Property(list, notify=joinBubbleChanged)
def joinBubble(self) -> LeafletPoly:
return self._join_bubble
@Property(list, notify=excludedZonesChanged)
def excludedZones(self) -> list[LeafletPoly]:
return self._excluded_zones
@Property(list, notify=permissibleZonesChanged)
def permissibleZones(self) -> list[LeafletPoly]:
return self._permissible_zones
@Property(list, notify=preferredLinesChanged)
def preferredLines(self) -> list[list[LeafletLatLon]]:
return self._preferred_lines
@classmethod
def empty(cls) -> HoldZonesJs:
return HoldZonesJs([], [], [], [], [], [])
@classmethod
def for_flight(cls, flight: Flight, game: Game) -> HoldZonesJs:
if not ENABLE_EXPENSIVE_DEBUG_TOOLS:
return JoinZonesJs.empty()
target = flight.package.target
home = flight.departure
if flight.package.waypoints is None:
return HoldZonesJs.empty()
ip = flight.package.waypoints.ingress
join = flight.package.waypoints.join
geometry = HoldZoneGeometry(
target.position, home.position, ip, join, game.blue, game.theater
)
return HoldZonesJs(
shapely_poly_to_leaflet_points(geometry.home_bubble, game.theater),
shapely_poly_to_leaflet_points(geometry.target_bubble, game.theater),
shapely_poly_to_leaflet_points(geometry.join_bubble, game.theater),
shapely_to_leaflet_polys(geometry.excluded_zones, game.theater),
shapely_to_leaflet_polys(geometry.permissible_zones, game.theater),
shapely_lines_to_leaflet_points(geometry.preferred_lines, game.theater),
)
class MapModel(QObject):
cleared = Signal()
@@ -754,6 +1026,9 @@ class MapModel(QObject):
navmeshesChanged = Signal()
mapZonesChanged = Signal()
unculledZonesChanged = Signal()
ipZonesChanged = Signal()
joinZonesChanged = Signal()
holdZonesChanged = Signal()
def __init__(self, game_model: GameModel) -> None:
super().__init__()
@@ -770,6 +1045,9 @@ class MapModel(QObject):
self._navmeshes = NavMeshJs([], [])
self._map_zones = MapZonesJs([], [], [])
self._unculled_zones = []
self._ip_zones = IpZonesJs.empty()
self._join_zones = JoinZonesJs.empty()
self._hold_zones = HoldZonesJs.empty()
self._selected_flight_index: Optional[Tuple[int, int]] = None
GameUpdateSignal.get_instance().game_loaded.connect(self.on_game_load)
GameUpdateSignal.get_instance().flight_paths_changed.connect(self.reset_atos)
@@ -793,6 +1071,7 @@ class MapModel(QObject):
self._navmeshes = NavMeshJs([], [])
self._map_zones = MapZonesJs([], [], [])
self._unculled_zones = []
self._ip_zones = IpZonesJs.empty()
self.cleared.emit()
def set_package_selection(self, index: int) -> None:
@@ -868,11 +1147,30 @@ class MapModel(QObject):
)
return flights
def _get_selected_flight(self) -> Optional[Flight]:
for p_idx, package in enumerate(self.game.blue.ato.packages):
for f_idx, flight in enumerate(package.flights):
if (p_idx, f_idx) == self._selected_flight_index:
return flight
return None
def reset_atos(self) -> None:
self._flights = self._flights_in_ato(
self.game.blue_ato, blue=True
) + self._flights_in_ato(self.game.red_ato, blue=False)
self.game.blue.ato, blue=True
) + self._flights_in_ato(self.game.red.ato, blue=False)
self.flightsChanged.emit()
selected_flight = self._get_selected_flight()
if selected_flight is None:
self._ip_zones = IpZonesJs.empty()
self._join_zones = JoinZonesJs.empty()
self._hold_zones = HoldZonesJs.empty()
else:
self._ip_zones = IpZonesJs.for_flight(selected_flight, self.game)
self._join_zones = JoinZonesJs.for_flight(selected_flight, self.game)
self._hold_zones = HoldZonesJs.for_flight(selected_flight, self.game)
self.ipZonesChanged.emit()
self.joinZonesChanged.emit()
self.holdZonesChanged.emit()
@Property(list, notify=flightsChanged)
def flights(self) -> List[FlightJs]:
@@ -1001,6 +1299,18 @@ class MapModel(QObject):
def unculledZones(self) -> list[UnculledZone]:
return self._unculled_zones
@Property(IpZonesJs, notify=ipZonesChanged)
def ipZones(self) -> IpZonesJs:
return self._ip_zones
@Property(JoinZonesJs, notify=joinZonesChanged)
def joinZones(self) -> JoinZonesJs:
return self._join_zones
@Property(HoldZonesJs, notify=holdZonesChanged)
def holdZones(self) -> HoldZonesJs:
return self._hold_zones
@property
def game(self) -> Game:
if self.game_model.game is None:

View File

@@ -0,0 +1,326 @@
from typing import Optional, Callable
from PySide2.QtCore import (
QItemSelectionModel,
QModelIndex,
QSize,
Qt,
QItemSelection,
Signal,
)
from PySide2.QtGui import QStandardItemModel, QStandardItem, QIcon
from PySide2.QtWidgets import (
QAbstractItemView,
QDialog,
QListView,
QVBoxLayout,
QGroupBox,
QLabel,
QWidget,
QScrollArea,
QLineEdit,
QTextEdit,
QCheckBox,
QHBoxLayout,
QStackedLayout,
QTabWidget,
)
from game import Game
from game.dcs.aircrafttype import AircraftType
from game.squadrons import Squadron, AirWing, Pilot
from gen.flights.flight import FlightType
from qt_ui.models import AirWingModel, SquadronModel
from qt_ui.uiconstants import AIRCRAFT_ICONS
from qt_ui.windows.AirWingDialog import SquadronDelegate
from qt_ui.windows.SquadronDialog import SquadronDialog
class SquadronList(QListView):
"""List view for displaying the air wing's squadrons."""
def __init__(self, air_wing_model: AirWingModel) -> None:
super().__init__()
self.air_wing_model = air_wing_model
self.dialog: Optional[SquadronDialog] = None
self.setIconSize(QSize(91, 24))
self.setItemDelegate(SquadronDelegate(self.air_wing_model))
self.setModel(self.air_wing_model)
self.selectionModel().setCurrentIndex(
self.air_wing_model.index(0, 0, QModelIndex()), QItemSelectionModel.Select
)
# self.setIconSize(QSize(91, 24))
self.setSelectionBehavior(QAbstractItemView.SelectItems)
self.doubleClicked.connect(self.on_double_click)
def on_double_click(self, index: QModelIndex) -> None:
if not index.isValid():
return
self.dialog = SquadronDialog(
SquadronModel(self.air_wing_model.squadron_at_index(index)), self
)
self.dialog.show()
class AllowedMissionTypeControls(QVBoxLayout):
def __init__(self, squadron: Squadron) -> None:
super().__init__()
self.squadron = squadron
self.allowed_mission_types = set()
self.addWidget(QLabel("Allowed mission types"))
def make_callback(toggled_task: FlightType) -> Callable[[bool], None]:
def callback(checked: bool) -> None:
self.on_toggled(toggled_task, checked)
return callback
for task in FlightType:
enabled = task in squadron.mission_types
if enabled:
self.allowed_mission_types.add(task)
checkbox = QCheckBox(text=task.value)
checkbox.setChecked(enabled)
checkbox.toggled.connect(make_callback(task))
self.addWidget(checkbox)
self.addStretch()
def on_toggled(self, task: FlightType, checked: bool) -> None:
if checked:
self.allowed_mission_types.add(task)
else:
self.allowed_mission_types.remove(task)
class SquadronConfigurationBox(QGroupBox):
def __init__(self, squadron: Squadron) -> None:
super().__init__()
self.setCheckable(True)
self.squadron = squadron
self.reset_title()
columns = QHBoxLayout()
self.setLayout(columns)
left_column = QVBoxLayout()
columns.addLayout(left_column)
left_column.addWidget(QLabel("Name:"))
self.name_edit = QLineEdit(squadron.name)
self.name_edit.textChanged.connect(self.on_name_changed)
left_column.addWidget(self.name_edit)
left_column.addWidget(QLabel("Nickname:"))
self.nickname_edit = QLineEdit(squadron.nickname)
self.nickname_edit.textChanged.connect(self.on_nickname_changed)
left_column.addWidget(self.nickname_edit)
if squadron.player:
player_label = QLabel(
"Players (one per line, leave empty for an AI-only squadron):"
)
else:
player_label = QLabel("Player slots not available for opfor")
left_column.addWidget(player_label)
players = [p for p in squadron.pilot_pool if p.player]
for player in players:
squadron.pilot_pool.remove(player)
if not squadron.player:
players = []
self.player_list = QTextEdit("<br />".join(p.name for p in players))
self.player_list.setAcceptRichText(False)
self.player_list.setEnabled(squadron.player)
left_column.addWidget(self.player_list)
left_column.addStretch()
self.allowed_missions = AllowedMissionTypeControls(squadron)
columns.addLayout(self.allowed_missions)
def on_name_changed(self, text: str) -> None:
self.squadron.name = text
self.reset_title()
def on_nickname_changed(self, text: str) -> None:
self.squadron.nickname = text
def reset_title(self) -> None:
self.setTitle(f"{self.squadron.name} - {self.squadron.aircraft}")
def apply(self) -> Squadron:
player_names = self.player_list.toPlainText().splitlines()
# Prepend player pilots so they get set active first.
self.squadron.pilot_pool = [
Pilot(n, player=True) for n in player_names
] + self.squadron.pilot_pool
self.squadron.mission_types = tuple(self.allowed_missions.allowed_mission_types)
return self.squadron
class SquadronConfigurationLayout(QVBoxLayout):
def __init__(self, squadrons: list[Squadron]) -> None:
super().__init__()
self.squadron_configs = []
for squadron in squadrons:
squadron_config = SquadronConfigurationBox(squadron)
self.squadron_configs.append(squadron_config)
self.addWidget(squadron_config)
def apply(self) -> list[Squadron]:
keep_squadrons = []
for squadron_config in self.squadron_configs:
if squadron_config.isChecked():
keep_squadrons.append(squadron_config.apply())
return keep_squadrons
class AircraftSquadronsPage(QWidget):
def __init__(self, squadrons: list[Squadron]) -> None:
super().__init__()
layout = QVBoxLayout()
self.setLayout(layout)
self.squadrons_config = SquadronConfigurationLayout(squadrons)
scrolling_widget = QWidget()
scrolling_widget.setLayout(self.squadrons_config)
scrolling_area = QScrollArea()
scrolling_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scrolling_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
scrolling_area.setWidgetResizable(True)
scrolling_area.setWidget(scrolling_widget)
layout.addWidget(scrolling_area)
def apply(self) -> list[Squadron]:
return self.squadrons_config.apply()
class AircraftSquadronsPanel(QStackedLayout):
def __init__(self, air_wing: AirWing) -> None:
super().__init__()
self.air_wing = air_wing
self.squadrons_pages: dict[AircraftType, AircraftSquadronsPage] = {}
for aircraft, squadrons in self.air_wing.squadrons.items():
page = AircraftSquadronsPage(squadrons)
self.addWidget(page)
self.squadrons_pages[aircraft] = page
def apply(self) -> None:
for aircraft, page in self.squadrons_pages.items():
self.air_wing.squadrons[aircraft] = page.apply()
class AircraftTypeList(QListView):
page_index_changed = Signal(int)
def __init__(self, air_wing: AirWing) -> None:
super().__init__()
self.setIconSize(QSize(91, 24))
self.setMinimumWidth(300)
model = QStandardItemModel(self)
self.setModel(model)
self.selectionModel().setCurrentIndex(
model.index(0, 0), QItemSelectionModel.Select
)
self.selectionModel().selectionChanged.connect(self.on_selection_changed)
for aircraft in air_wing.squadrons:
aircraft_item = QStandardItem(aircraft.name)
icon = self.icon_for(aircraft)
if icon is not None:
aircraft_item.setIcon(icon)
aircraft_item.setEditable(False)
aircraft_item.setSelectable(True)
model.appendRow(aircraft_item)
def on_selection_changed(
self, selected: QItemSelection, _deselected: QItemSelection
) -> None:
indexes = selected.indexes()
if len(indexes) > 1:
raise RuntimeError("Aircraft list should not allow multi-selection")
if not indexes:
return
self.page_index_changed.emit(indexes[0].row())
@staticmethod
def icon_for(aircraft: AircraftType) -> Optional[QIcon]:
name = aircraft.dcs_id
if name in AIRCRAFT_ICONS:
return QIcon(AIRCRAFT_ICONS[name])
return None
class AirWingConfigurationTab(QWidget):
def __init__(self, air_wing: AirWing) -> None:
super().__init__()
layout = QHBoxLayout()
self.setLayout(layout)
type_list = AircraftTypeList(air_wing)
type_list.page_index_changed.connect(self.on_aircraft_changed)
layout.addWidget(type_list)
self.squadrons_panel = AircraftSquadronsPanel(air_wing)
layout.addLayout(self.squadrons_panel)
def apply(self) -> None:
self.squadrons_panel.apply()
def on_aircraft_changed(self, index: QModelIndex) -> None:
self.squadrons_panel.setCurrentIndex(index)
class AirWingConfigurationDialog(QDialog):
"""Dialog window for air wing configuration."""
def __init__(self, game: Game, parent) -> None:
super().__init__(parent)
self.setMinimumSize(500, 800)
self.setWindowTitle(f"Air Wing Configuration")
# TODO: self.setWindowIcon()
layout = QVBoxLayout()
self.setLayout(layout)
doc_url = (
"https://github.com/dcs-liberation/dcs_liberation/wiki/Squadrons-and-pilots"
)
doc_label = QLabel(
"Use this opportunity to customize the squadrons available to your "
"coalition. <strong>This is your only opportunity to make changes.</strong>"
"<br /><br />"
"To accept your changes and continue, close this window.<br />"
"<br />"
"To remove a squadron from the game, uncheck the box in the title. New "
"squadrons cannot be added via the UI at this time. To add a custom "
"squadron,<br />"
f'see <a style="color:#ffffff" href="{doc_url}">the wiki</a>.'
)
doc_label.setOpenExternalLinks(True)
layout.addWidget(doc_label)
tab_widget = QTabWidget()
layout.addWidget(tab_widget)
self.tabs = []
for coalition in game.coalitions:
coalition_tab = AirWingConfigurationTab(coalition.air_wing)
name = "Blue" if coalition.player else "Red"
tab_widget.addTab(coalition_tab, name)
self.tabs.append(coalition_tab)
def reject(self) -> None:
for tab in self.tabs:
tab.apply()
super().reject()

View File

@@ -3,12 +3,7 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import Optional, Iterator
from PySide2.QtCore import (
QItemSelectionModel,
QModelIndex,
Qt,
QSize,
)
from PySide2.QtCore import QItemSelectionModel, QModelIndex, QSize
from PySide2.QtWidgets import (
QAbstractItemView,
QCheckBox,
@@ -183,7 +178,7 @@ class AirInventoryView(QWidget):
self.table.setSortingEnabled(True)
def iter_allocated_aircraft(self) -> Iterator[AircraftInventoryData]:
for package in self.game_model.game.blue_ato.packages:
for package in self.game_model.game.blue.ato.packages:
for flight in package.flights:
yield from AircraftInventoryData.from_flight(flight)

View File

@@ -36,6 +36,8 @@ from qt_ui.windows.preferences.QLiberationPreferencesWindow import (
)
from qt_ui.windows.settings.QSettingsWindow import QSettingsWindow
from qt_ui.windows.stats.QStatsWindow import QStatsWindow
from qt_ui.windows.notes.QNotesWindow import QNotesWindow
from qt_ui.windows.logs.QLogsWindow import QLogsWindow
class QLiberationWindow(QMainWindow):
@@ -150,6 +152,9 @@ class QLiberationWindow(QMainWindow):
)
)
self.openLogsAction = QAction("Show &logs", self)
self.openLogsAction.triggered.connect(self.showLogsDialog)
self.openSettingsAction = QAction("Settings", self)
self.openSettingsAction.setIcon(CONST.ICONS["Settings"])
self.openSettingsAction.triggered.connect(self.showSettingsDialog)
@@ -158,6 +163,10 @@ class QLiberationWindow(QMainWindow):
self.openStatsAction.setIcon(CONST.ICONS["Statistics"])
self.openStatsAction.triggered.connect(self.showStatsDialog)
self.openNotesAction = QAction("Notes", self)
self.openNotesAction.setIcon(CONST.ICONS["Notes"])
self.openNotesAction.triggered.connect(self.showNotesDialog)
def initToolbar(self):
self.tool_bar = self.addToolBar("File")
self.tool_bar.addAction(self.newGameAction)
@@ -171,6 +180,7 @@ class QLiberationWindow(QMainWindow):
self.actions_bar = self.addToolBar("Actions")
self.actions_bar.addAction(self.openSettingsAction)
self.actions_bar.addAction(self.openStatsAction)
self.actions_bar.addAction(self.openNotesAction)
def initMenuBar(self):
self.menu = self.menuBar()
@@ -204,6 +214,7 @@ class QLiberationWindow(QMainWindow):
help_menu.addAction(
"Report an &issue", lambda: webbrowser.open_new_tab(URLS["Issues"])
)
help_menu.addAction(self.openLogsAction)
help_menu.addSeparator()
help_menu.addAction(self.showAboutDialogAction)
@@ -351,6 +362,14 @@ class QLiberationWindow(QMainWindow):
self.dialog = QStatsWindow(self.game)
self.dialog.show()
def showNotesDialog(self):
self.dialog = QNotesWindow(self.game)
self.dialog.show()
def showLogsDialog(self):
self.dialog = QLogsWindow()
self.dialog.show()
def onDebriefing(self, debrief: Debriefing):
logging.info("On Debriefing")
self.debriefing = QDebriefingWindow(debrief)

View File

@@ -94,6 +94,9 @@ class QUnitInfoWindow(QDialog):
self.details_text = QTextBrowser()
self.details_text.setProperty("style", "info-desc")
self.details_text.setText(unit_type.description)
self.details_text.setOpenExternalLinks(
True
) # in aircrafttype.py and groundunittype.py, for the descriptions, if No Data. including a google search link
self.gridLayout.addWidget(self.details_text, 3, 0)
self.layout.addLayout(self.gridLayout, 1, 0)

View File

@@ -73,11 +73,15 @@ class DepartingConvoysList(QFrame):
task_box_layout = QGridLayout()
scroll_content.setLayout(task_box_layout)
for convoy in game_model.game.transfers.convoys.departing_from(cp):
for convoy in game_model.game.coalition_for(
cp.captured
).transfers.convoys.departing_from(cp):
group_info = DepartingConvoyInfo(convoy)
task_box_layout.addWidget(group_info)
for cargo_ship in game_model.game.transfers.cargo_ships.departing_from(cp):
for cargo_ship in game_model.game.coalition_for(
cp.captured
).transfers.cargo_ships.departing_from(cp):
group_info = DepartingConvoyInfo(cargo_ship)
task_box_layout.addWidget(group_info)

View File

@@ -108,7 +108,7 @@ class QBaseMenu2(QDialog):
capture_button.clicked.connect(self.cheat_capture)
self.budget_display = QLabel(
QRecruitBehaviour.BUDGET_FORMAT.format(self.game_model.game.budget)
QRecruitBehaviour.BUDGET_FORMAT.format(self.game_model.game.blue.budget)
)
self.budget_display.setAlignment(Qt.AlignRight | Qt.AlignBottom)
self.budget_display.setProperty("style", "budget-label")
@@ -124,7 +124,6 @@ class QBaseMenu2(QDialog):
self.cp.capture(self.game_model.game, for_player=not self.cp.captured)
# Reinitialized ground planners and the like. The ATO needs to be reset because
# missions planned against the flipped base are no longer valid.
self.game_model.game.reset_ato()
self.game_model.game.initialize_turn()
GameUpdateSignal.get_instance().updateGame(self.game_model.game)
@@ -140,7 +139,7 @@ class QBaseMenu2(QDialog):
@property
def can_afford_runway_repair(self) -> bool:
return self.game_model.game.budget >= db.RUNWAY_REPAIR_COST
return self.game_model.game.blue.budget >= db.RUNWAY_REPAIR_COST
def begin_runway_repair(self) -> None:
if not self.can_afford_runway_repair:
@@ -148,7 +147,7 @@ class QBaseMenu2(QDialog):
self,
"Cannot repair runway",
f"Runway repair costs ${db.RUNWAY_REPAIR_COST}M but you have "
f"only ${self.game_model.game.budget}M available.",
f"only ${self.game_model.game.blue.budget}M available.",
QMessageBox.Ok,
)
return
@@ -162,7 +161,7 @@ class QBaseMenu2(QDialog):
return
self.cp.begin_runway_repair()
self.game_model.game.budget -= db.RUNWAY_REPAIR_COST
self.game_model.game.blue.budget -= db.RUNWAY_REPAIR_COST
self.update_repair_button()
self.update_intel_summary()
GameUpdateSignal.get_instance().updateGame(self.game_model.game)
@@ -196,7 +195,9 @@ class QBaseMenu2(QDialog):
ground_unit_limit = self.cp.frontline_unit_count_limit
deployable_unit_info = ""
allocated = self.cp.allocated_ground_units(self.game_model.game.transfers)
allocated = self.cp.allocated_ground_units(
self.game_model.game.coalition_for(self.cp.captured).transfers
)
unit_overage = max(
allocated.total_present - self.cp.frontline_unit_count_limit, 0
)
@@ -256,4 +257,6 @@ class QBaseMenu2(QDialog):
NewUnitTransferDialog(self.game_model, self.cp, parent=self.window()).show()
def update_budget(self, game: Game) -> None:
self.budget_display.setText(QRecruitBehaviour.BUDGET_FORMAT.format(game.budget))
self.budget_display.setText(
QRecruitBehaviour.BUDGET_FORMAT.format(game.blue.budget)
)

View File

@@ -103,11 +103,11 @@ class QRecruitBehaviour:
@property
def budget(self) -> float:
return self.game_model.game.budget
return self.game_model.game.blue.budget
@budget.setter
def budget(self, value: int) -> None:
self.game_model.game.budget = value
self.game_model.game.blue.budget = value
def add_purchase_row(
self,
@@ -209,8 +209,6 @@ class QRecruitBehaviour:
if self.pending_deliveries.available_next_turn(unit_type) > 0:
self.budget += unit_type.price
self.pending_deliveries.sell({unit_type: 1})
if self.pending_deliveries.units[unit_type] == 0:
del self.pending_deliveries.units[unit_type]
self.update_purchase_controls()
self.update_available_budget()
return True

View File

@@ -45,7 +45,7 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
row = 0
unit_types: Set[AircraftType] = set()
for unit_type in self.game_model.game.player_faction.aircrafts:
for unit_type in self.game_model.game.blue.faction.aircrafts:
if self.cp.is_carrier and not unit_type.carrier_capable:
continue
if self.cp.is_lha and not unit_type.lha_capable:

View File

@@ -56,6 +56,5 @@ class QGroundForcesStrategy(QGroupBox):
self.cp.base.affect_strength(amount)
enemy_point.base.affect_strength(-amount)
# Clear the ATO to replan missions affected by the front line.
self.game.reset_ato()
self.game.initialize_turn()
GameUpdateSignal.get_instance().updateGame(self.game)

View File

@@ -57,10 +57,7 @@ class FinancesLayout(QGridLayout):
middle=f"Income multiplier: {income.multiplier:.1f}",
right=f"<b>{income.total}M</b>",
)
if player:
budget = game.budget
else:
budget = game.enemy_budget
budget = game.coalition_for(player).budget
self.add_row(middle="Balance", right=f"<b>{budget}M</b>")
self.setRowStretch(next(self.row), 1)

View File

@@ -1,7 +1,6 @@
import logging
from typing import List, Optional
from PySide2 import QtCore
from PySide2.QtGui import Qt
from PySide2.QtWidgets import (
QComboBox,
@@ -238,8 +237,8 @@ class QGroundObjectMenu(QDialog):
self.total_value = total_value
def repair_unit(self, group, unit, price):
if self.game.budget > price:
self.game.budget -= price
if self.game.blue.budget > price:
self.game.blue.budget -= price
group.units_losts = [u for u in group.units_losts if u.id != unit.id]
group.units.append(unit)
GameUpdateSignal.get_instance().updateGame(self.game)
@@ -257,8 +256,16 @@ class QGroundObjectMenu(QDialog):
def sell_all(self):
self.update_total_value()
self.game.budget = self.game.budget + self.total_value
self.game.blue.budget = self.game.blue.budget + self.total_value
self.ground_object.groups = []
# Replan if the tgo was a target of the redfor
if any(
package.target == self.ground_object
for package in self.game.ato_for(player=False).packages
):
self.game.initialize_turn(for_red=True, for_blue=False)
self.do_refresh_layout()
GameUpdateSignal.get_instance().updateGame(self.game)
@@ -299,14 +306,17 @@ class QBuyGroupForGroundObjectDialog(QDialog):
self.buySamBox = QGroupBox("Buy SAM site :")
self.buyArmorBox = QGroupBox("Buy defensive position :")
faction = self.game.player_faction
faction = self.game.blue.faction
# Sams
possible_sams = get_faction_possible_sams_generator(faction)
for sam in possible_sams:
# Pre Generate SAM to get the real price
generator = sam(self.game, self.ground_object)
generator.generate()
self.samCombo.addItem(
sam.name + " [$" + str(sam.price) + "M]", userData=sam
generator.name + " [$" + str(generator.price) + "M]", userData=generator
)
self.samCombo.currentIndexChanged.connect(self.samComboChanged)
@@ -331,8 +341,12 @@ class QBuyGroupForGroundObjectDialog(QDialog):
buy_ewr_layout.addWidget(self.ewr_selector, 0, 1, alignment=Qt.AlignRight)
ewr_types = get_faction_possible_ewrs_generator(faction)
for ewr_type in ewr_types:
# Pre Generate to get the real price
generator = ewr_type(self.game, self.ground_object)
generator.generate()
self.ewr_selector.addItem(
f"{ewr_type.name()} [${ewr_type.price()}M]", ewr_type
generator.name() + " [$" + str(generator.price) + "M]",
userData=generator,
)
self.ewr_selector.currentIndexChanged.connect(self.on_ewr_selection_changed)
@@ -402,7 +416,7 @@ class QBuyGroupForGroundObjectDialog(QDialog):
def on_ewr_selection_changed(self, index):
ewr = self.ewr_selector.itemData(index)
self.buy_ewr_button.setText(
f"Buy [${ewr.price()}M][-${self.current_group_value}M]"
f"Buy [${ewr.price}M][-${self.current_group_value}M]"
)
def armorComboChanged(self, index):
@@ -419,12 +433,12 @@ class QBuyGroupForGroundObjectDialog(QDialog):
logging.info("Buying Armor ")
utype = self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())
price = utype.price * self.amount.value() - self.current_group_value
if price > self.game.budget:
if price > self.game.blue.budget:
self.error_money()
self.close()
return
else:
self.game.budget -= price
self.game.blue.budget -= price
# Generate Armor
group = generate_armor_group_of_type_and_size(
@@ -432,36 +446,40 @@ class QBuyGroupForGroundObjectDialog(QDialog):
)
self.ground_object.groups = [group]
# Replan redfor missions
self.game.initialize_turn(for_red=True, for_blue=False)
GameUpdateSignal.get_instance().updateGame(self.game)
def buySam(self):
sam_generator = self.samCombo.itemData(self.samCombo.currentIndex())
price = sam_generator.price - self.current_group_value
if price > self.game.budget:
if price > self.game.blue.budget:
self.error_money()
return
else:
self.game.budget -= price
self.game.blue.budget -= price
# Generate SAM
generator = sam_generator(self.game, self.ground_object)
generator.generate()
self.ground_object.groups = list(generator.groups)
self.ground_object.groups = list(sam_generator.groups)
# Replan redfor missions
self.game.initialize_turn(for_red=True, for_blue=False)
GameUpdateSignal.get_instance().updateGame(self.game)
def buy_ewr(self):
ewr_generator = self.ewr_selector.itemData(self.ewr_selector.currentIndex())
price = ewr_generator.price() - self.current_group_value
if price > self.game.budget:
price = ewr_generator.price - self.current_group_value
if price > self.game.blue.budget:
self.error_money()
return
else:
self.game.budget -= price
self.game.blue.budget -= price
generator = ewr_generator(self.game, self.ground_object)
generator.generate()
self.ground_object.groups = [generator.vg]
self.ground_object.groups = [ewr_generator.vg]
# Replan redfor missions
self.game.initialize_turn(for_red=True, for_blue=False)
GameUpdateSignal.get_instance().updateGame(self.game)

View File

@@ -0,0 +1,67 @@
import logging
import typing
from PySide2.QtWidgets import (
QDialog,
QPlainTextEdit,
QVBoxLayout,
QPushButton,
)
from PySide2.QtGui import QTextCursor
from qt_ui.logging_handler import HookableInMemoryHandler
class QLogsWindow(QDialog):
vbox: QVBoxLayout
textbox: QPlainTextEdit
clear_button: QPushButton
_logging_handler: typing.Optional[HookableInMemoryHandler]
def __init__(self):
super().__init__()
self.setWindowTitle("Logs")
self.setMinimumSize(400, 100)
self.resize(1000, 450)
self.vbox = QVBoxLayout()
self.setLayout(self.vbox)
self.textbox = QPlainTextEdit(self)
self.textbox.setReadOnly(True)
self.textbox.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
self.textbox.move(10, 10)
self.textbox.resize(1000, 450)
self.textbox.setStyleSheet(
"font-family: 'Courier New', monospace; background: #1D2731;"
)
self.vbox.addWidget(self.textbox)
self.clear_button = QPushButton(self)
self.clear_button.setText("CLEAR")
self.clear_button.setProperty("style", "btn-primary")
self.clear_button.clicked.connect(self.clearLogs)
self.vbox.addWidget(self.clear_button)
self._logging_handler = None
logger = logging.getLogger()
for handler in logger.handlers:
if isinstance(handler, HookableInMemoryHandler):
self._logging_handler = handler
break
if self._logging_handler is not None:
self.textbox.setPlainText(self._logging_handler.log)
self.textbox.moveCursor(QTextCursor.End)
self._logging_handler.setHook(self.appendLog)
else:
self.textbox.setPlainText("WARNING: logging not initialized!")
def clearLogs(self) -> None:
if self._logging_handler is not None:
self._logging_handler.clearLog()
self.textbox.setPlainText("")
def appendLog(self, msg: str):
self.textbox.appendPlainText(msg)
self.textbox.moveCursor(QTextCursor.End)

View File

@@ -180,7 +180,7 @@ class QPackageDialog(QDialog):
self.game.aircraft_inventory.claim_for_flight(flight)
self.package_model.add_flight(flight)
planner = FlightPlanBuilder(
self.game, self.package_model.package, is_player=True
self.package_model.package, self.game.blue, self.game.theater
)
try:
planner.populate_flight_plan(flight)

View File

@@ -38,7 +38,7 @@ class QFlightCreator(QDialog):
self.game = game
self.package = package
self.custom_name_text = None
self.country = self.game.player_country
self.country = self.game.blue.country_name
self.setWindowTitle("Create flight")
self.setWindowIcon(EVENT_ICONS["strike"])
@@ -52,7 +52,6 @@ class QFlightCreator(QDialog):
self.aircraft_selector = QAircraftTypeSelector(
self.game.aircraft_inventory.available_types_for_player,
self.game.player_country,
self.task_selector.currentData(),
)
self.aircraft_selector.setCurrentIndex(0)

View File

@@ -47,8 +47,20 @@ class QFlightPayloadTab(QFrame):
def reload_from_flight(self) -> None:
self.loadout_selector.setCurrentText(self.flight.loadout.name)
def loadout_at(self, index: int) -> Loadout:
loadout = self.loadout_selector.itemData(index)
if loadout is None:
return Loadout.empty_loadout()
return loadout
def current_loadout(self) -> Loadout:
loadout = self.loadout_selector.currentData()
if loadout is None:
return Loadout.empty_loadout()
return loadout
def on_new_loadout(self, index: int) -> None:
self.flight.loadout = self.loadout_selector.itemData(index)
self.flight.loadout = self.loadout_at(index)
self.payload_editor.reset_pylons()
def on_custom_toggled(self, use_custom: bool) -> None:
@@ -56,5 +68,5 @@ class QFlightPayloadTab(QFrame):
if use_custom:
self.flight.loadout = self.flight.loadout.derive_custom("Custom")
else:
self.flight.loadout = self.loadout_selector.currentData()
self.flight.loadout = self.current_loadout()
self.payload_editor.reset_pylons()

View File

@@ -56,7 +56,7 @@ class QPylonEditor(QComboBox):
#
# A similar hack exists in Pylon to support forcibly equipping this even when
# it's not known to be compatible.
if weapon.cls_id == "<CLEAN>":
if weapon.clsid == "<CLEAN>":
if not self.has_added_clean_item:
self.addItem("Clean", weapon)
self.has_added_clean_item = True

View File

@@ -100,6 +100,6 @@ class FlightAirfieldDisplay(QGroupBox):
def update_flight_plan(self) -> None:
planner = FlightPlanBuilder(
self.game, self.package_model.package, is_player=True
self.package_model.package, self.game.blue, self.game.theater
)
planner.populate_flight_plan(self.flight)

View File

@@ -37,7 +37,7 @@ class QFlightWaypointTab(QFrame):
self.game = game
self.package = package
self.flight = flight
self.planner = FlightPlanBuilder(self.game, package, is_player=True)
self.planner = FlightPlanBuilder(package, game.blue, game.theater)
self.flight_waypoint_list: Optional[QFlightWaypointList] = None
self.rtb_waypoint: Optional[QPushButton] = None

View File

@@ -15,6 +15,7 @@ from game.theater.start_generator import GameGenerator, GeneratorSettings, ModSe
from game.factions.faction import Faction
from qt_ui.widgets.QLiberationCalendar import QLiberationCalendar
from qt_ui.widgets.spinsliders import TenthsSpinSlider, TimeInputs, CurrencySpinner
from qt_ui.windows.AirWingConfigurationDialog import AirWingConfigurationDialog
from qt_ui.windows.newgame.QCampaignList import (
Campaign,
QCampaignList,
@@ -125,6 +126,10 @@ class NewGameWizard(QtWidgets.QWizard):
)
self.generatedGame = generator.generate()
AirWingConfigurationDialog(self.generatedGame, self).exec_()
self.generatedGame.begin_turn_0()
super(NewGameWizard, self).accept()

View File

@@ -0,0 +1,67 @@
from PySide2.QtWidgets import (
QDialog,
QPlainTextEdit,
QVBoxLayout,
QHBoxLayout,
QPushButton,
QLabel,
)
from PySide2.QtGui import QTextCursor
from PySide2.QtCore import QTimer
import qt_ui.uiconstants as CONST
from game.game import Game
from time import sleep
class QNotesWindow(QDialog):
def __init__(self, game: Game):
super(QNotesWindow, self).__init__()
self.game = game
self.setWindowTitle("Notes")
self.setWindowIcon(CONST.ICONS["Notes"])
self.setMinimumSize(400, 100)
self.resize(600, 450)
self.vbox = QVBoxLayout()
self.setLayout(self.vbox)
self.vbox.addWidget(
QLabel("Saved notes are available as a page in your kneeboard.")
)
self.textbox = QPlainTextEdit(self)
try:
self.textbox.setPlainText(self.game.notes)
self.textbox.moveCursor(QTextCursor.End)
except AttributeError: # old save may not have game.notes
pass
self.textbox.move(10, 10)
self.textbox.resize(600, 450)
self.textbox.setStyleSheet("background: #1D2731;")
self.vbox.addWidget(self.textbox)
self.button_row = QHBoxLayout()
self.vbox.addLayout(self.button_row)
self.clear_button = QPushButton(self)
self.clear_button.setText("CLEAR")
self.clear_button.setProperty("style", "btn-primary")
self.clear_button.clicked.connect(self.clearNotes)
self.button_row.addWidget(self.clear_button)
self.save_button = QPushButton(self)
self.save_button.setText("SAVE")
self.save_button.setProperty("style", "btn-success")
self.save_button.clicked.connect(self.saveNotes)
self.button_row.addWidget(self.save_button)
def clearNotes(self) -> None:
self.textbox.setPlainText("")
def saveNotes(self) -> None:
self.game.notes = self.textbox.toPlainText()
self.save_button.setText("SAVED")
QTimer.singleShot(5000, lambda: self.save_button.setText("SAVE"))

View File

@@ -58,6 +58,12 @@ class QLiberationFirstStartWindow(QDialog):
<p>As you click on the button below, the file will be replaced in your DCS installation directory.</p>
<br/>
<p>If you leave the DCS Installation Directory empty, DCS Liberation can not automatically replace the MissionScripting.lua and will therefore not work correctly!
In this case, you need to edit the file yourself. The easiest way to do it is to replace the original file with the file in dcs-liberation distribution (&lt;dcs_liberation_installation&gt;/resources/scripts/MissionScripting.lua).
<br/><br/>You can find more information on how to manually change this file in the Liberation Wiki (Page: Dedicated Server Guide) on GitHub.</p>
<br/><br/>
<strong>Thank you for reading !</strong>

View File

@@ -22,6 +22,7 @@ class QLiberationPreferences(QFrame):
super(QLiberationPreferences, self).__init__()
self.saved_game_dir = ""
self.dcs_install_dir = ""
self.install_dir_ignore_warning = False
self.dcs_install_dir = liberation_install.get_dcs_install_directory()
self.saved_game_dir = liberation_install.get_saved_game_dir()
@@ -102,17 +103,38 @@ class QLiberationPreferences(QFrame):
error_dialog.exec_()
return False
if not os.path.isdir(self.dcs_install_dir):
if self.install_dir_ignore_warning and self.dcs_install_dir == "":
warning_dialog = QMessageBox.warning(
self,
"The DCS Installation directory was not set",
"You set an empty DCS Installation directory! "
"<br/><br/>Without this directory, DCS Liberation can not replace the MissionScripting.lua for you and will not work properly. "
"In this case, you need to edit the MissionScripting.lua yourself. The easiest way to do it is to replace the original file (&lt;dcs_installation_directory&gt;/Scripts/MissionScripting.lua) with the file in dcs-liberation distribution (&lt;dcs_liberation_installation&gt;/resources/scripts/MissionScripting.lua)."
"<br/><br/>You can find more information on how to manually change this file in the Liberation Wiki (Page: Dedicated Server Guide) on GitHub.</p>"
"<br/><br/>Are you sure that you want to leave the installation directory empty?"
"<br/><br/><strong>This is only recommended for expert users!</strong>",
QMessageBox.StandardButton.Yes,
QMessageBox.StandardButton.No,
)
if warning_dialog == QMessageBox.No:
return False
elif not os.path.isdir(self.dcs_install_dir):
error_dialog = QMessageBox.critical(
self,
"Wrong DCS installation directory.",
self.dcs_install_dir + " is not a valid directory",
self.dcs_install_dir
+ " is not a valid directory. DCS Liberation requires the installation directory to replace the MissionScripting.lua"
"<br/><br/>If you ignore this Error, DCS Liberation can not work properly and needs your attention. "
"In this case, you need to edit the MissionScripting.lua yourself. The easiest way to do it is to replace the original file (&lt;dcs_installation_directory&gt;/Scripts/MissionScripting.lua) with the file in dcs-liberation distribution (&lt;dcs_liberation_installation&gt;/resources/scripts/MissionScripting.lua)."
"<br/><br/>You can find more information on how to manually change this file in the Liberation Wiki (Page: Dedicated Server Guide) on GitHub.</p>"
"<br/><br/><strong>This is only recommended for expert users!</strong>",
QMessageBox.StandardButton.Ignore,
QMessageBox.StandardButton.Ok,
)
error_dialog.exec_()
if error_dialog == QMessageBox.Ignore:
self.install_dir_ignore_warning = True
return False
if not os.path.isdir(
elif not os.path.isdir(
os.path.join(self.dcs_install_dir, "Scripts")
) and os.path.isfile(os.path.join(self.dcs_install_dir, "bin", "DCS.exe")):
error_dialog = QMessageBox.critical(

View File

@@ -101,7 +101,7 @@ class HqAutomationSettingsBox(QGroupBox):
front_line = QCheckBox()
front_line.setChecked(self.game.settings.automate_front_line_reinforcements)
front_line.toggled.connect(self.set_front_line_automation)
front_line.toggled.connect(self.set_front_line_reinforcement_automation)
layout.addWidget(QLabel("Automate front-line purchases"), 1, 0)
layout.addWidget(front_line, 1, 1, Qt.AlignRight)
@@ -147,12 +147,30 @@ class HqAutomationSettingsBox(QGroupBox):
)
layout.addWidget(self.auto_ato_player_missions_asap, 4, 1, Qt.AlignRight)
self.automate_front_line_stance = QCheckBox()
self.automate_front_line_stance.setChecked(
self.game.settings.automate_front_line_stance
)
self.automate_front_line_stance.toggled.connect(
self.set_front_line_stance_automation
)
layout.addWidget(
QLabel("Automatically manage front line stances"),
5,
0,
)
layout.addWidget(self.automate_front_line_stance, 5, 1, Qt.AlignRight)
def set_runway_automation(self, value: bool) -> None:
self.game.settings.automate_runway_repair = value
def set_front_line_automation(self, value: bool) -> None:
def set_front_line_reinforcement_automation(self, value: bool) -> None:
self.game.settings.automate_front_line_reinforcements = value
def set_front_line_stance_automation(self, value: bool) -> None:
self.game.settings.automate_front_line_stance = value
def set_aircraft_automation(self, value: bool) -> None:
self.game.settings.automate_aircraft_reinforcements = value
@@ -855,7 +873,7 @@ class QSettingsWindow(QDialog):
def cheatMoney(self, amount):
logging.info("CHEATING FOR AMOUNT : " + str(amount) + "M")
self.game.budget += amount
self.game.blue.budget += amount
if amount > 0:
self.game.informations.append(
Information(

View File

@@ -42,10 +42,16 @@ class QAircraftChart(QFrame):
self.chart.setTitle("Aircraft forces over time")
self.chart.createDefaultAxes()
self.chart.axisX().setTitleText("Turn")
self.chart.axisX().setLabelFormat("%i")
self.chart.axisX().setRange(0, len(self.alliedAircraft))
self.chart.axisX().applyNiceNumbers()
self.chart.axisY().setLabelFormat("%i")
self.chart.axisY().setRange(
0, max(max(self.alliedAircraft), max(self.enemyAircraft)) + 10
)
self.chart.axisY().applyNiceNumbers()
self.chartView = QtCharts.QChartView(self.chart)
self.chartView.setRenderHint(QPainter.Antialiasing)

View File

@@ -42,10 +42,16 @@ class QArmorChart(QFrame):
self.chart.setTitle("Combat vehicles over time")
self.chart.createDefaultAxes()
self.chart.axisX().setTitleText("Turn")
self.chart.axisX().setLabelFormat("%i")
self.chart.axisX().setRange(0, len(self.alliedArmor))
self.chart.axisX().applyNiceNumbers()
self.chart.axisY().setLabelFormat("%i")
self.chart.axisY().setRange(
0, max(max(self.alliedArmor), max(self.enemyArmor)) + 10
)
self.chart.axisY().applyNiceNumbers()
self.chartView = QtCharts.QChartView(self.chart)
self.chartView.setRenderHint(QPainter.Antialiasing)

View File

@@ -14,7 +14,7 @@ class QStatsWindow(QDialog):
self.setModal(True)
self.setWindowTitle("Stats")
self.setWindowIcon(CONST.ICONS["Statistics"])
self.setMinimumSize(600, 250)
self.setMinimumSize(600, 300)
self.layout = QGridLayout()
self.aircraft_charts = QAircraftChart(self.game)