mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge remote-tracking branch 'upstream/develop' into new-plugin-system
This commit is contained in:
commit
1bd26005f2
@ -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)
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 = []
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user