Update the UI to show sim state.

https://github.com/dcs-liberation/dcs_liberation/issues/1704
This commit is contained in:
Dan Albert 2021-10-31 22:25:11 -07:00
parent 87bf3110c8
commit 03430a4df5
12 changed files with 188 additions and 39 deletions

View File

@ -1,6 +1,7 @@
from __future__ import annotations
import logging
from datetime import datetime
from pathlib import Path
from typing import Callable, TYPE_CHECKING
@ -22,6 +23,10 @@ class GameLoop:
self.started = False
self.completed = False
@property
def current_time_in_sim(self) -> datetime:
return self.sim.time
def start(self) -> None:
if self.started:
raise RuntimeError("Cannot start game loop because it has already started")
@ -67,7 +72,5 @@ class GameLoop:
self.pause()
logging.info(f"Simulation completed at {self.sim.time}")
self.on_complete()
else:
logging.info(f"Simulation continued at {self.sim.time}")
except SimulationAlreadyCompletedError:
logging.exception("Attempted to tick already completed sim")

View File

@ -32,6 +32,7 @@ class MissionSimulation:
self.time = self.game.conditions.start_time
def begin_simulation(self) -> None:
self.time = self.game.conditions.start_time
self.aircraft_simulation.begin_simulation()
def tick(self) -> bool:

View File

@ -1,14 +1,16 @@
from __future__ import annotations
import logging
from datetime import datetime
from pathlib import Path
from typing import Callable, Optional, TYPE_CHECKING
from PySide2.QtCore import QObject, Signal
from game.polldebriefingfilethread import PollDebriefingFileThread
from game.sim.gameloop import GameLoop
from game.sim.simspeedsetting import SimSpeedSetting
from game.polldebriefingfilethread import PollDebriefingFileThread
from qt_ui.simupdatethread import SimUpdateThread
if TYPE_CHECKING:
from game import Game
@ -16,6 +18,7 @@ if TYPE_CHECKING:
class SimController(QObject):
sim_update = Signal()
sim_speed_reset = Signal(SimSpeedSetting)
simulation_complete = Signal()
@ -24,18 +27,28 @@ class SimController(QObject):
self.game_loop: Optional[GameLoop] = None
self.recreate_game_loop(game)
self.started = False
self._sim_update_thread = SimUpdateThread(self.sim_update.emit)
self._sim_update_thread.start()
@property
def completed(self) -> bool:
return self.game_loop.completed
@property
def current_time_in_sim(self) -> Optional[datetime]:
if self.game_loop is None:
return None
return self.game_loop.current_time_in_sim
def set_game(self, game: Optional[Game]) -> None:
self.recreate_game_loop(game)
self.sim_speed_reset.emit(SimSpeedSetting.PAUSED)
def recreate_game_loop(self, game: Optional[Game]) -> None:
if self.game_loop is not None:
self._sim_update_thread.on_sim_pause()
self.game_loop.pause()
self.game_loop = None
if game is not None:
self.game_loop = GameLoop(game, self.on_simulation_complete)
self.started = False
@ -48,11 +61,17 @@ class SimController(QObject):
self.game_loop.start()
self.started = True
self.game_loop.set_simulation_speed(simulation_speed)
if simulation_speed is SimSpeedSetting.PAUSED:
self._sim_update_thread.on_sim_pause()
else:
self._sim_update_thread.on_sim_unpause()
def run_to_first_contact(self) -> None:
self.game_loop.run_to_first_contact()
self.sim_update.emit()
def generate_miz(self, output: Path) -> None:
self._sim_update_thread.on_sim_pause()
self.game_loop.pause_and_generate_miz(output)
def wait_for_debriefing(
@ -65,11 +84,14 @@ class SimController(QObject):
def debrief_current_state(
self, state_path: Path, force_end: bool = False
) -> Debriefing:
self._sim_update_thread.on_sim_pause()
return self.game_loop.pause_and_debrief(state_path, force_end)
def process_results(self, debriefing: Debriefing) -> None:
self._sim_update_thread.on_sim_pause()
return self.game_loop.complete_with_results(debriefing)
def on_simulation_complete(self) -> None:
logging.debug("Simulation complete")
self._sim_update_thread.on_sim_pause()
self.simulation_complete.emit()

45
qt_ui/simupdatethread.py Normal file
View File

@ -0,0 +1,45 @@
from threading import Event, Thread, Timer
from typing import Callable
class SimUpdateThread(Thread):
def __init__(self, update_callback: Callable[[], None]) -> None:
super().__init__()
self.update_callback = update_callback
self.running = False
self.should_shutdown = False
self._interrupt = Event()
self._timer = self._make_timer()
def run(self) -> None:
while True:
self._interrupt.wait()
self._interrupt.clear()
if self.should_shutdown:
return
if self.running:
self.update_callback()
self._timer = self._make_timer()
self._timer.start()
def on_sim_pause(self) -> None:
self._timer.cancel()
self._timer = self._make_timer()
self.running = False
def on_sim_unpause(self) -> None:
if not self.running:
self.running = True
self._timer.start()
def stop(self) -> None:
self.should_shutdown = True
self._interrupt.set()
def on_timer_elapsed(self) -> None:
self._timer = self._make_timer()
self._timer.start()
self._interrupt.set()
def _make_timer(self) -> Timer:
return Timer(1 / 60, lambda: self._interrupt.set())

View File

@ -1,3 +1,5 @@
from datetime import datetime
from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import (
QFrame,
@ -12,6 +14,7 @@ from dcs.weather import CloudPreset, Weather as PydcsWeather
import qt_ui.uiconstants as CONST
from game.utils import mps
from game.weather import Conditions, TimeOfDay
from qt_ui.simcontroller import SimController
class QTimeTurnWidget(QGroupBox):
@ -19,8 +22,9 @@ class QTimeTurnWidget(QGroupBox):
UI Component to display current turn and time info
"""
def __init__(self):
def __init__(self, sim_controller: SimController) -> None:
super(QTimeTurnWidget, self).__init__("Turn")
self.sim_controller = sim_controller
self.setStyleSheet(
"padding: 0px; margin-left: 5px; margin-right: 0px; margin-top: 1ex; margin-bottom: 5px; border-right: 0px"
)
@ -49,17 +53,30 @@ class QTimeTurnWidget(QGroupBox):
self.time_display = QLabel()
self.time_column.addWidget(self.time_display)
def setCurrentTurn(self, turn: int, conditions: Conditions) -> None:
sim_controller.sim_update.connect(self.on_sim_update)
def on_sim_update(self) -> None:
time = self.sim_controller.current_time_in_sim
if time is None:
self.date_display.setText("")
self.time_display.setText("")
else:
self.set_date_and_time(time)
def set_current_turn(self, turn: int, conditions: Conditions) -> None:
"""Sets the turn information display.
:arg turn Current turn number.
:arg conditions Current time and weather conditions.
"""
self.daytime_icon.setPixmap(self.icons[conditions.time_of_day])
self.date_display.setText(conditions.start_time.strftime("%d %b %Y"))
self.time_display.setText(conditions.start_time.strftime("%H:%M:%S Local"))
self.set_date_and_time(conditions.start_time)
self.setTitle(f"Turn {turn}")
def set_date_and_time(self, time: datetime) -> None:
self.date_display.setText(time.strftime("%d %b %Y"))
self.time_display.setText(time.strftime("%H:%M:%S Local"))
class QWeatherWidget(QGroupBox):
"""
@ -265,7 +282,7 @@ class QConditionsWidget(QFrame):
UI Component to display Turn Number, Day Time & Hour and weather combined.
"""
def __init__(self):
def __init__(self, sim_controller: SimController) -> None:
super(QConditionsWidget, self).__init__()
self.setProperty("style", "QConditionsWidget")
@ -275,7 +292,7 @@ class QConditionsWidget(QFrame):
self.layout.setVerticalSpacing(0)
self.setLayout(self.layout)
self.time_turn_widget = QTimeTurnWidget()
self.time_turn_widget = QTimeTurnWidget(sim_controller)
self.time_turn_widget.setStyleSheet("QGroupBox { margin-right: 0px; }")
self.layout.addWidget(self.time_turn_widget, 0, 0)
@ -292,6 +309,6 @@ class QConditionsWidget(QFrame):
:arg turn Current turn number.
:arg conditions Current time and weather conditions.
"""
self.time_turn_widget.setCurrentTurn(turn, conditions)
self.time_turn_widget.set_current_turn(turn, conditions)
self.weather_widget.setCurrentTurn(turn, conditions)
self.weather_widget.show()

View File

@ -38,7 +38,7 @@ class QTopPanel(QFrame):
self.setMaximumHeight(70)
self.conditionsWidget = QConditionsWidget()
self.conditionsWidget = QConditionsWidget(sim_controller)
self.budgetBox = QBudgetBox(self.game)
pass_turn_text = "Pass Turn"

View File

@ -15,6 +15,7 @@ from PySide2.QtWebEngineWidgets import (
from game import Game
from qt_ui.models import GameModel
from qt_ui.simcontroller import SimController
from qt_ui.widgets.map.mapmodel import MapModel
@ -35,11 +36,13 @@ class LoggingWebPage(QWebEnginePage):
class QLiberationMap(QWebEngineView):
def __init__(self, game_model: GameModel, parent) -> None:
def __init__(
self, game_model: GameModel, sim_controller: SimController, parent
) -> None:
super().__init__(parent)
self.game_model = game_model
self.setMinimumSize(800, 600)
self.map_model = MapModel(game_model)
self.map_model = MapModel(game_model, sim_controller)
self.channel = QWebChannel()
self.channel.registerObject("game", self.map_model)

View File

@ -2,7 +2,7 @@ from __future__ import annotations
import logging
from datetime import timedelta
from typing import List, Optional, Tuple, Union, Iterator
from typing import Iterator, List, Optional, Tuple, Union
from PySide2.QtCore import Property, QObject, Signal, Slot
from dcs import Point
@ -10,40 +10,38 @@ from dcs.unit import Unit
from dcs.vehicles import vehicle_map
from shapely.geometry import (
LineString,
MultiLineString,
MultiPolygon,
Point as ShapelyPoint,
Polygon,
MultiPolygon,
MultiLineString,
)
from game import Game
from game.ato.airtaaskingorder import AirTaskingOrder
from game.ato.flight import Flight
from game.ato.flightstate import InFlight
from game.ato.flightwaypoint import FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType
from game.dcs.groundunittype import GroundUnitType
from game.flightplan import JoinZoneGeometry, HoldZoneGeometry
from game.flightplan import HoldZoneGeometry, JoinZoneGeometry
from game.flightplan.ipzonegeometry import IpZoneGeometry
from game.navmesh import NavMesh, NavMeshPoly
from game.profiling import logged_duration
from game.theater import (
ConflictTheater,
ControlPoint,
TheaterGroundObject,
ControlPointStatus,
FrontLine,
LatLon,
ControlPointStatus,
TheaterGroundObject,
)
from game.threatzones import ThreatZones
from game.transfers import MultiGroupTransport, TransportMap
from game.utils import meters, nautical_miles
from game.ato.airtaaskingorder import AirTaskingOrder
from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.flightwaypoint import FlightWaypoint
from game.ato.flight import Flight
from gen.flights.flightplan import (
FlightPlan,
PatrollingFlightPlan,
CasFlightPlan,
)
from game.flightplan.ipzonegeometry import IpZoneGeometry
from gen.flights.flightplan import CasFlightPlan, FlightPlan, PatrollingFlightPlan
from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel, AtoModel
from qt_ui.models import AtoModel, GameModel
from qt_ui.simcontroller import SimController
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
@ -537,6 +535,7 @@ class WaypointJs(QObject):
class FlightJs(QObject):
positionChanged = Signal()
flightPlanChanged = Signal()
blueChanged = Signal()
selectedChanged = Signal()
@ -588,6 +587,13 @@ class FlightJs(QObject):
waypoints.append(waypoint)
return waypoints
@Property(list, notify=positionChanged)
def position(self) -> LeafletLatLon:
if isinstance(self.flight.state, InFlight):
ll = self.theater.point_to_ll(self.flight.state.estimate_position())
return [ll.latitude, ll.longitude]
return []
@Property(list, notify=flightPlanChanged)
def flightPlan(self) -> List[WaypointJs]:
return self._waypoints
@ -1032,7 +1038,7 @@ class MapModel(QObject):
joinZonesChanged = Signal()
holdZonesChanged = Signal()
def __init__(self, game_model: GameModel) -> None:
def __init__(self, game_model: GameModel, sim_controller: SimController) -> None:
super().__init__()
self.game_model = game_model
self._map_center = [0, 0]
@ -1059,6 +1065,7 @@ class MapModel(QObject):
GameUpdateSignal.get_instance().flight_selection_changed.connect(
self.set_flight_selection
)
sim_controller.sim_update.connect(self.on_sim_update)
self.reset()
def clear(self) -> None:
@ -1076,6 +1083,10 @@ class MapModel(QObject):
self._ip_zones = IpZonesJs.empty()
self.cleared.emit()
def on_sim_update(self) -> None:
for flight in self._flights:
flight.positionChanged.emit()
def set_package_selection(self, index: int) -> None:
# Optional[int] isn't a valid type for a Qt signal. None will be converted to
# zero automatically. We use -1 to indicate no selection.

View File

@ -54,7 +54,7 @@ class QLiberationWindow(QMainWindow):
self.sim_controller = SimController(self.game)
self.ato_panel = QAirTaskingOrderPanel(self.game_model)
self.info_panel = QInfoPanel(self.game)
self.liberation_map = QLiberationMap(self.game_model, self)
self.liberation_map = QLiberationMap(self.game_model, self.sim_controller, self)
self.setGeometry(300, 100, 270, 100)
self.updateWindowTitle()

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.2" baseProfile="tiny" width="118" height="128" viewBox="41 26 118 128"><path d="M 155,150 C 155,50 115,30 100,30 85,30 45,50 45,150" stroke-width="4" stroke="black" fill="rgb(128,224,255)" fill-opacity="1" ></path></svg>

After

Width:  |  Height:  |  Size: 271 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.2" baseProfile="tiny" width="118" height="138" viewBox="41 16 118 138"><path d="M 45,150 L45,70 100,20 155,70 155,150" stroke-width="4" stroke="black" fill="rgb(255,128,128)" fill-opacity="1" ></path></svg>

After

Width:  |  Height:  |  Size: 257 B

View File

@ -104,9 +104,31 @@ class TgoIcons {
}
}
class AirIcons {
constructor() {
this.icons = {};
for (const player of [true, false]) {
this.icons[player] = this.loadIcon("unspecified", player);
}
}
icon(_category, player, _state) {
return this.icons[player];
}
loadIcon(category, player) {
const color = player ? "blue" : "red";
return new L.Icon({
iconUrl: `../air_assets/${category}_${color}.svg`,
iconSize: [24, 24],
});
}
}
const Icons = Object.freeze({
ControlPoints: new CpIcons(),
Objectives: new TgoIcons(),
AirIcons: new AirIcons(),
});
function metersToNauticalMiles(meters) {
@ -163,6 +185,7 @@ defaultBaseMap.addTo(map);
// Enabled by default, so addTo(map).
const controlPointsLayer = L.layerGroup().addTo(map);
const aircraftLayer = L.layerGroup().addTo(map);
const airDefensesLayer = L.layerGroup().addTo(map);
const factoriesLayer = L.layerGroup().addTo(map);
const shipsLayer = L.layerGroup().addTo(map);
@ -249,8 +272,9 @@ L.control
.groupedLayers(
baseLayers,
{
"Points of Interest": {
"Units and locations": {
"Control points": controlPointsLayer,
Aircraft: aircraftLayer,
"Air defenses": airDefensesLayer,
Factories: factoriesLayer,
Ships: shipsLayer,
@ -307,7 +331,7 @@ new QWebChannel(qt.webChannelTransport, function (channel) {
game.groundObjectsChanged.connect(drawGroundObjects);
game.supplyRoutesChanged.connect(drawSupplyRoutes);
game.frontLinesChanged.connect(drawFrontLines);
game.flightsChanged.connect(drawFlightPlans);
game.flightsChanged.connect(drawAircraft);
game.threatZonesChanged.connect(drawThreatZones);
game.navmeshesChanged.connect(drawNavmeshes);
game.mapZonesChanged.connect(drawMapZones);
@ -770,9 +794,11 @@ class Flight {
constructor(flight) {
this.flight = flight;
this.flightPlan = this.flight.flightPlan.map((p) => new Waypoint(p, this));
this.aircraft = null;
this.path = null;
this.commitBoundary = null;
this.flight.flightPlanChanged.connect(() => this.draw());
this.flight.positionChanged.connect(() => this.drawAircraftLocation());
this.flight.flightPlanChanged.connect(() => this.drawFlightPlan());
this.flight.commitBoundaryChanged.connect(() => this.drawCommitBoundary());
}
@ -808,6 +834,25 @@ class Flight {
}
}
draw() {
this.drawAircraftLocation();
this.drawFlightPlan();
this.drawCommitBoundary();
}
drawAircraftLocation() {
if (this.aircraft != null) {
this.aircraft.removeFrom(aircraftLayer);
this.aircraft = null;
}
const position = this.flight.position;
if (position.length > 0) {
this.aircraft = L.marker(position, {
icon: Icons.AirIcons.icon("fighter", this.flight.blue),
}).addTo(aircraftLayer);
}
}
drawCommitBoundary() {
if (this.commitBoundary != null) {
this.commitBoundary
@ -829,7 +874,7 @@ class Flight {
}
}
draw() {
drawFlightPlan() {
const path = [];
this.flightPlan.forEach((waypoint) => {
if (waypoint.includeInPath()) {
@ -844,11 +889,11 @@ class Flight {
});
this.drawPath(path);
this.drawCommitBoundary();
}
}
function drawFlightPlans() {
function drawAircraft() {
aircraftLayer.clearLayers();
blueFlightPlansLayer.clearLayers();
redFlightPlansLayer.clearLayers();
selectedFlightPlansLayer.clearLayers();
@ -1136,7 +1181,7 @@ function drawInitialMap() {
drawGroundObjects();
drawSupplyRoutes();
drawFrontLines();
drawFlightPlans();
drawAircraft();
drawThreatZones();
drawNavmeshes();
drawMapZones();