Merge remote-tracking branch 'upstream/develop' into new-plugin-system

This commit is contained in:
David Pierron 2020-10-20 22:10:19 +02:00
commit 1bd26005f2
12 changed files with 188 additions and 87 deletions

View File

@ -71,7 +71,7 @@ class Weather:
return WindConditions(
# Always some wind to make the smoke move a bit.
at_0m=Wind(wind_direction, min(1, base_wind * at_0m_factor)),
at_0m=Wind(wind_direction, max(1, base_wind * at_0m_factor)),
at_2000m=Wind(wind_direction, base_wind * at_2000m_factor),
at_8000m=Wind(wind_direction, base_wind * at_8000m_factor)
)

View File

@ -1140,8 +1140,9 @@ class HoldPointBuilder(PydcsWaypointBuilder):
altitude=waypoint.alt,
pattern=OrbitAction.OrbitPattern.Circle
))
loiter.stop_after_time(
self.timing.push_time(self.flight, self.waypoint))
push_time = self.timing.push_time(self.flight, self.waypoint)
self.waypoint.departure_time = push_time
loiter.stop_after_time(push_time)
waypoint.add_task(loiter)
return waypoint

View File

@ -390,9 +390,6 @@ class CoalitionMissionPlanner:
MAX_SEAD_RANGE = nm_to_meter(150)
MAX_STRIKE_RANGE = nm_to_meter(150)
NON_CAP_MIN_DELAY = 1
NON_CAP_MAX_DELAY = 5
def __init__(self, game: Game, is_player: bool) -> None:
self.game = game
self.is_player = is_player

View File

@ -1,9 +1,11 @@
from enum import Enum
from typing import Dict, Iterable, Optional
from typing import Dict, Iterable, List, Optional
from dcs.mapping import Point
from dcs.point import MovingPoint, PointAction
from dcs.unittype import UnitType
from game import db
from dcs.unittype import UnitType
from dcs.point import MovingPoint, PointAction
from theater.controlpoint import ControlPoint, MissionTarget
@ -91,17 +93,22 @@ class FlightWaypoint:
self.only_for_player = False
self.data = None
# This is set very late by the air conflict generator (part of mission
# These are set very late by the air conflict generator (part of mission
# generation). We do it late so that we don't need to propagate changes
# to waypoint times whenever the player alters the package TOT or the
# flight's offset in the UI.
self.tot: Optional[int] = None
self.departure_time: Optional[int] = None
@property
def position(self) -> Point:
return Point(self.x, self.y)
@classmethod
def from_pydcs(cls, point: MovingPoint,
from_cp: ControlPoint) -> "FlightWaypoint":
waypoint = FlightWaypoint(point.position.x, point.position.y,
point.alt)
waypoint = FlightWaypoint(FlightWaypointType.NAV, point.position.x,
point.position.y, point.alt)
waypoint.alt_type = point.alt_type
# Other actions exist... but none of them *should* be the first
# waypoint for a flight.
@ -159,14 +166,3 @@ class Flight:
if waypoint.waypoint_type in types:
return waypoint
return None
# Test
if __name__ == '__main__':
from dcs.planes import A_10C
from theater import ControlPoint, Point, List
from_cp = ControlPoint(0, "AA", Point(0, 0), Point(0, 0), [], 0, 0)
f = Flight(A_10C(), 4, from_cp, FlightType.CAS, "Cold")
f.scheduled_in = 50
print(f)

View File

@ -187,6 +187,10 @@ class FlightPlanBuilder:
closest_cache = ObjectiveDistanceCache.get_closest_airfields(location)
for airfield in closest_cache.closest_airfields:
# If the mission is a BARCAP of an enemy airfield, find the *next*
# closest enemy airfield.
if airfield == self.package.target:
continue
if airfield.captured != self.is_player:
closest_airfield = airfield
break
@ -198,10 +202,19 @@ class FlightPlanBuilder:
closest_airfield.position
)
min_distance_from_enemy = nm_to_meter(20)
distance_to_airfield = int(closest_airfield.position.distance_to_point(
self.package.target.position
))
distance_to_no_fly = distance_to_airfield - min_distance_from_enemy
min_cap_distance = min(self.doctrine.cap_min_distance_from_cp,
distance_to_no_fly)
max_cap_distance = min(self.doctrine.cap_max_distance_from_cp,
distance_to_no_fly)
end = location.position.point_from_heading(
heading,
random.randint(self.doctrine.cap_min_distance_from_cp,
self.doctrine.cap_max_distance_from_cp)
random.randint(min_cap_distance, max_cap_distance)
)
diameter = random.randint(
self.doctrine.cap_min_track_length,

View File

@ -29,16 +29,19 @@ from pathlib import Path
from typing import Dict, List, Optional, Tuple
from PIL import Image, ImageDraw, ImageFont
from dcs.mapping import Point
from dcs.mission import Mission
from dcs.unittype import FlyingType
from tabulate import tabulate
from game.utils import meter_to_nm
from . import units
from .aircraft import AIRCRAFT_DATA, FlightData
from .airfields import RunwayData
from .airsupportgen import AwacsInfo, TankerInfo
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
from .flights.flight import FlightWaypoint, FlightWaypointType
from .flights.traveltime import TravelTime
from .radios import RadioFrequency
@ -111,6 +114,7 @@ class FlightPlanBuilder:
self.start_time = start_time
self.rows: List[List[str]] = []
self.target_points: List[NumberedWaypoint] = []
self.last_waypoint: Optional[FlightWaypoint] = None
def add_waypoint(self, waypoint_num: int, waypoint: FlightWaypoint) -> None:
if waypoint.waypoint_type == FlightWaypointType.TARGET_POINT:
@ -136,22 +140,59 @@ class FlightPlanBuilder:
f"{first_waypoint_num}-{last_waypoint_num}",
"Target points",
"0",
self._waypoint_distance(self.target_points[0].waypoint),
self._ground_speed(self.target_points[0].waypoint),
self._format_time(self.target_points[0].waypoint.tot),
self._format_time(self.target_points[0].waypoint.departure_time),
])
self.last_waypoint = self.target_points[-1].waypoint
def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None:
self.rows.append([
str(waypoint.number),
waypoint.waypoint.pretty_name,
str(int(units.meters_to_feet(waypoint.waypoint.alt))),
self._waypoint_distance(waypoint.waypoint),
self._ground_speed(waypoint.waypoint),
self._format_time(waypoint.waypoint.tot),
self._format_time(waypoint.waypoint.departure_time),
])
self.last_waypoint = waypoint.waypoint
def _format_time(self, time: Optional[int]) -> str:
if time is None:
return ""
local_time = self.start_time + datetime.timedelta(seconds=time)
return local_time.strftime(f"%H:%M:%S LOCAL")
return local_time.strftime(f"%H:%M:%S")
def _waypoint_distance(self, waypoint: FlightWaypoint) -> str:
if self.last_waypoint is None:
return "-"
distance = meter_to_nm(self.last_waypoint.position.distance_to_point(
waypoint.position
))
return f"{distance} NM"
def _ground_speed(self, waypoint: FlightWaypoint) -> str:
if self.last_waypoint is None:
return "-"
if waypoint.tot is None:
return "-"
if self.last_waypoint.departure_time is not None:
last_time = self.last_waypoint.departure_time
elif self.last_waypoint.tot is not None:
last_time = self.last_waypoint.tot
else:
return "-"
distance = meter_to_nm(self.last_waypoint.position.distance_to_point(
waypoint.position
))
duration = (waypoint.tot - last_time) / 3600
return f"{int(distance / duration)} kt"
def build(self) -> List[List[str]]:
return self.rows
@ -186,8 +227,9 @@ class BriefingPage(KneeboardPage):
flight_plan_builder = FlightPlanBuilder(self.start_time)
for num, waypoint in enumerate(self.flight.waypoints):
flight_plan_builder.add_waypoint(num, waypoint)
writer.table(flight_plan_builder.build(),
headers=["STPT", "Action", "Alt", "TOT"])
writer.table(flight_plan_builder.build(), headers=[
"#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure"
])
writer.heading("Comm Ladder")
comms = []

View File

@ -337,6 +337,7 @@ class QPackageList(QListView):
self.setItemDelegate(PackageDelegate())
self.setIconSize(QSize(91, 24))
self.setSelectionBehavior(QAbstractItemView.SelectItems)
self.model().rowsInserted.connect(self.on_new_packages)
@property
def selected_item(self) -> Optional[Package]:
@ -346,6 +347,14 @@ class QPackageList(QListView):
return None
return self.ato_model.package_at_index(index)
def on_new_packages(self, _parent: QModelIndex, first: int,
_last: int) -> None:
# Select the newly created pacakges. This should only ever happen due to
# the player saving a new package, so selecting it helps them view/edit
# it faster.
self.selectionModel().setCurrentIndex(self.model().index(first, 0),
QItemSelectionModel.Select)
class QPackagePanel(QGroupBox):
"""The package display portion of the ATO panel.
@ -357,7 +366,7 @@ class QPackagePanel(QGroupBox):
def __init__(self, model: AtoModel) -> None:
super().__init__("Packages")
self.ato_model = model
self.ato_model.layoutChanged.connect(self.on_selection_changed)
self.ato_model.layoutChanged.connect(self.on_current_changed)
self.vbox = QVBoxLayout()
self.setLayout(self.vbox)
@ -378,15 +387,15 @@ class QPackagePanel(QGroupBox):
self.delete_button.clicked.connect(self.on_delete)
self.button_row.addWidget(self.delete_button)
self.selection_changed.connect(self.on_selection_changed)
self.on_selection_changed()
self.current_changed.connect(self.on_current_changed)
self.on_current_changed()
@property
def selection_changed(self):
def current_changed(self):
"""Returns the signal emitted when the flight selection changes."""
return self.package_list.selectionModel().selectionChanged
return self.package_list.selectionModel().currentChanged
def on_selection_changed(self) -> None:
def on_current_changed(self) -> None:
"""Updates the status of the edit and delete buttons."""
index = self.package_list.currentIndex()
enabled = index.isValid()
@ -436,8 +445,7 @@ class QAirTaskingOrderPanel(QSplitter):
self.ato_model = game_model.ato_model
self.package_panel = QPackagePanel(self.ato_model)
self.package_panel.selection_changed.connect(self.on_package_change)
self.ato_model.rowsInserted.connect(self.on_package_change)
self.package_panel.current_changed.connect(self.on_package_change)
self.addWidget(self.package_panel)
self.flight_panel = QFlightPanel(game_model)

View File

@ -39,3 +39,9 @@ class QOriginAirfieldSelector(QComboBox):
self.addItem(f"{origin.name} ({available} available)", origin)
self.model().sort(0)
self.update()
@property
def available(self) -> int:
origin = self.currentData()
inventory = self.global_inventory.for_control_point(origin)
return inventory.available(self.aircraft)

View File

@ -4,8 +4,16 @@ import datetime
import logging
from typing import List, Optional, Tuple
from PySide2.QtCore import Qt, QPointF
from PySide2.QtGui import QBrush, QColor, QPen, QPixmap, QWheelEvent, QPolygonF
from PySide2.QtCore import QPointF, Qt
from PySide2.QtGui import (
QBrush,
QColor,
QFont,
QPen,
QPixmap,
QPolygonF,
QWheelEvent,
)
from PySide2.QtWidgets import (
QFrame,
QGraphicsItem,
@ -46,6 +54,9 @@ class QLiberationMap(QGraphicsView):
self.game_model = game_model
self.game: Optional[Game] = game_model.game
self.waypoint_info_font = QFont()
self.waypoint_info_font.setPointSize(12)
self.flight_path_items: List[QGraphicsItem] = []
# A tuple of (package index, flight index), or none.
self.selected_flight: Optional[Tuple[int, int]] = None
@ -345,19 +356,19 @@ class QLiberationMap(QGraphicsView):
pen = QPen(QColor("black"), 0.3)
brush = QColor("white")
def draw_text(text: str, x: int, y: int) -> None:
item = scene.addSimpleText(text)
item.setBrush(brush)
item.setPen(pen)
item.moveBy(x, y)
item.setZValue(2)
self.flight_path_items.append(item)
text = "\n".join([
f"{number} {waypoint.name}",
f"{altitude} ft {altitude_type}",
tot,
])
draw_text(f"{number} {waypoint.name}", position[0] + 8,
position[1] - 15)
draw_text(f"{altitude} ft {altitude_type}", position[0] + 8,
position[1] - 5)
draw_text(tot, position[0] + 8, position[1] + 5)
item = scene.addSimpleText(text, self.waypoint_info_font)
item.setFlag(QGraphicsItem.ItemIgnoresTransformations)
item.setBrush(brush)
item.setPen(pen)
item.moveBy(position[0] + 8, position[1])
item.setZValue(2)
self.flight_path_items.append(item)
def draw_flight_path(self, scene: QGraphicsScene, pos0: Tuple[int, int],
pos1: Tuple[int, int], player: bool,

View File

@ -102,7 +102,7 @@ class QPackageDialog(QDialog):
self.delete_flight_button = QPushButton("Delete Selected")
self.delete_flight_button.setProperty("style", "btn-danger")
self.delete_flight_button.clicked.connect(self.on_delete_flight)
self.delete_flight_button.setEnabled(False)
self.delete_flight_button.setEnabled(model.rowCount() > 0)
self.button_layout.addWidget(self.delete_flight_button)
self.button_layout.addStretch()
@ -205,7 +205,7 @@ class QNewPackageDialog(QPackageDialog):
def on_cancel(self) -> None:
super().on_cancel()
for flight in self.package_model.package.flights:
self.game_model.game.aircraft_inventory.return_from_flight(flight)
self.game.aircraft_inventory.return_from_flight(flight)
class QEditPackageDialog(QPackageDialog):

View File

@ -53,10 +53,23 @@ class QFlightCreator(QDialog):
[cp for cp in game.theater.controlpoints if cp.captured],
self.aircraft_selector.currentData()
)
self.aircraft_selector.currentIndexChanged.connect(self.update_max_size)
layout.addLayout(QLabeledWidget("Airfield:", self.airfield_selector))
self.flight_size_spinner = QFlightSizeSpinner()
layout.addLayout(QLabeledWidget("Count:", self.flight_size_spinner))
self.update_max_size()
layout.addLayout(QLabeledWidget("Size:", self.flight_size_spinner))
self.client_slots_spinner = QFlightSizeSpinner(
min_size=0,
max_size=self.flight_size_spinner.value(),
default_size=0
)
self.flight_size_spinner.valueChanged.connect(
lambda v: self.client_slots_spinner.setMaximum(v)
)
layout.addLayout(
QLabeledWidget("Client Slots:", self.client_slots_spinner))
layout.addStretch()
@ -96,6 +109,7 @@ class QFlightCreator(QDialog):
start_type = "Warm"
flight = Flight(aircraft, size, origin, task, start_type)
flight.scheduled_in = self.package.delay
flight.client_count = self.client_slots_spinner.value()
# noinspection PyUnresolvedReferences
self.created.emit(flight)
@ -104,3 +118,8 @@ class QFlightCreator(QDialog):
def on_aircraft_changed(self, index: int) -> None:
new_aircraft = self.aircraft_selector.itemData(index)
self.airfield_selector.change_aircraft(new_aircraft)
def update_max_size(self) -> None:
self.flight_size_spinner.setMaximum(
min(self.airfield_selector.available, 4)
)

View File

@ -39,46 +39,54 @@ write_state = function()
-- messageAll("Done writing DCS Liberation state.")
end
debriefing_file_location = nil
if dcsLiberation then
debriefing_file_location = dcsLiberation.installPath
end
if debriefing_file_location then
logger:info("Using DCS Liberation install folder for state.json")
else
local function discoverDebriefingFilePath()
local function insertFileName(directoryOrFilePath, overrideFileName)
if overrideFileName then
logger:info("Using LIBERATION_EXPORT_STAMPED_STATE to locate the state.json")
return directoryOrFilePath .. os.time() .. "-state.json"
end
local filename = "state.json"
if not (directoryOrFilePath:sub(-#filename) == filename) then
return directoryOrFilePath .. filename
end
return directoryOrFilePath
end
-- establish a search pattern into the following modes
-- 1. Environment variable mode, to support dedicated server hosting
-- 2. Embedded DCS Liberation Generation, to support locally hosted single player
-- 3. Retain the classic TEMP directory logic
if os then
debriefing_file_location = os.getenv("LIBERATION_EXPORT_DIR")
if debriefing_file_location then debriefing_file_location = debriefing_file_location .. "\\" end
end
if debriefing_file_location then
logger:info("Using LIBERATION_EXPORT_DIR environment variable for state.json")
else
if os then
debriefing_file_location = os.getenv("TEMP")
if debriefing_file_location then debriefing_file_location = debriefing_file_location .. "\\" end
end
if debriefing_file_location then
logger:info("Using TEMP environment variable for state.json")
else
if lfs then
debriefing_file_location = lfs.writedir()
end
if debriefing_file_location then
logger:info("Using DCS working directory for state.json")
end
local exportDirectory = os.getenv("LIBERATION_EXPORT_DIR")
if exportDirectory then
logger:info("Using LIBERATION_EXPORT_DIR to locate the state.json")
local useCurrentStamping = os.getenv("LIBERATION_EXPORT_STAMPED_STATE")
exportDirectory = exportDirectory .. "\\"
return insertFileName(exportDirectory, useCurrentStamping)
end
end
end
if debriefing_file_location then
local filename = "state.json"
if not debriefing_file_location:sub(-#filename) == filename then
debriefing_file_location = debriefing_file_location .. filename
if dcsLiberation then
logger:info("Using DCS Liberation install folder for state.json")
return insertFileName(dcsLiberation.installPath)
end
if lfs then
logger:info("Using DCS working directory for state.json")
return insertFileName(lfs.writedir())
end
logger:info(string.format("DCS Liberation state will be written as json to [[%s]]",debriefing_file_location))
else
logger:error("No usable storage path for state.json")
end
debriefing_file_location = discoverDebriefingFilePath()
logger:info(string.format("DCS Liberation state will be written as json to [[%s]]",debriefing_file_location))
write_state_error_handling = function()
if pcall(write_state) then
-- messageAll("Written DCS Liberation state to "..debriefing_file_location)