diff --git a/changelog.md b/changelog.md index fd8bf492..a779e550 100644 --- a/changelog.md +++ b/changelog.md @@ -5,11 +5,12 @@ Saves from 13.x are not compatible with 14.0.0. ## Features/Improvements * **[Engine]** Support for DCS 2.9.16.10973 +* **[UI]** Allow saving after fast forwarding manually with sim speed controls (--show-sim-speed-controls option). ## Fixes +* **[Campaign]** Units are restored to full health when repaired. * **[UI]** Air Wing and Transfers buttons disabled when no game is loaded as pressing them without a game loaded resulted in a crash. -* **[UI]** Units are restored to full health when repaired. * **[UI]** A package is cancelled (deleted) when the last flight in the package is cancelled instead of showing "No mission". # 13.0.0 diff --git a/game/ato/flight.py b/game/ato/flight.py index 8ece14f2..220cc630 100644 --- a/game/ato/flight.py +++ b/game/ato/flight.py @@ -109,19 +109,6 @@ class Flight(SidcDescribable): waypoint.actions.clear() waypoint.options.clear() - def __getstate__(self) -> dict[str, Any]: - state = self.__dict__.copy() - # Avoid persisting the flight state since that's not (currently) used outside - # mission generation. This is a bit of a hack for the moment and in the future - # we will need to persist the flight state, but for now keep it out of save - # compat (it also contains a generator that cannot be pickled). - del state["state"] - return state - - def __setstate__(self, state: dict[str, Any]) -> None: - state["state"] = Uninitialized(self, state["squadron"].settings) - self.__dict__.update(state) - @property def blue(self) -> bool: return self.squadron.player diff --git a/game/ato/flightstate/flightstate.py b/game/ato/flightstate/flightstate.py index 040d568b..6672cb31 100644 --- a/game/ato/flightstate/flightstate.py +++ b/game/ato/flightstate/flightstate.py @@ -21,8 +21,14 @@ class FlightState(ABC): self.settings = settings self.avoid_further_combat = False - def reinitialize(self, now: datetime) -> None: - from game.ato.flightstate import WaitingForStart + def initialize(self, now: datetime) -> None: + from game.ato.flightstate import Uninitialized, WaitingForStart + + # Flight objects are created with Uninitialized state. However when the simulation runs + # the flight state changes and may be serialized. We only want to initialize the state + # for newly created flights and not ones deserialized from a save file. + if type(self.flight.state) != Uninitialized: + return if self.flight.flight_plan.startup_time() <= now: self._set_active_flight_state(now) diff --git a/game/ato/flightstate/uninitialized.py b/game/ato/flightstate/uninitialized.py index e3f0b09a..e42f57a2 100644 --- a/game/ato/flightstate/uninitialized.py +++ b/game/ato/flightstate/uninitialized.py @@ -20,7 +20,7 @@ class Uninitialized(FlightState): def on_game_tick( self, events: GameUpdateEvents, time: datetime, duration: timedelta ) -> None: - self.reinitialize(time) + self.initialize(time) self.flight.state.on_game_tick(events, time, duration) @property diff --git a/game/game.py b/game/game.py index f7716e21..91f6aec9 100644 --- a/game/game.py +++ b/game/game.py @@ -125,7 +125,11 @@ class Game: self.time_of_day_offset_for_start_time = list(TimeOfDay).index( self.theater.daytime_map.best_guess_time_of_day_at(start_time) ) + # self.conditions.start_time is the time at the start of a turn and does not change within a turn. + # self.simulation_time tracks time progression within a turn and is synchronized with the + # MissionSimulation object. self.conditions = self.generate_conditions(forced_time=start_time) + self.simulation_time = self.conditions.start_time self.sanitize_sides(player_faction, enemy_faction) self.blue = Coalition(self, player_faction, player_budget, player=True) @@ -291,6 +295,7 @@ class Game: # turn 1. if self.turn > 1: self.conditions = self.generate_conditions() + self.simulation_time = self.conditions.start_time def begin_turn_0(self) -> None: """Initialization for the first turn of the game.""" diff --git a/game/sim/aircraftsimulation.py b/game/sim/aircraftsimulation.py index 82f62200..099680b6 100644 --- a/game/sim/aircraftsimulation.py +++ b/game/sim/aircraftsimulation.py @@ -27,7 +27,6 @@ class AircraftSimulation: self.results = SimulationResults() def begin_simulation(self) -> None: - self.reset() self.set_initial_flight_states() def on_game_tick( @@ -72,14 +71,9 @@ class AircraftSimulation: events.complete_simulation() def set_initial_flight_states(self) -> None: - now = self.game.conditions.start_time + now = self.game.simulation_time for flight in self.iter_flights(): - flight.state.reinitialize(now) - - def reset(self) -> None: - for flight in self.iter_flights(): - flight.set_state(Uninitialized(flight, self.game.settings)) - self.combats = [] + flight.state.initialize(now) def iter_flights(self) -> Iterator[Flight]: packages = itertools.chain( diff --git a/game/sim/missionsimulation.py b/game/sim/missionsimulation.py index cef32e19..90a562c9 100644 --- a/game/sim/missionsimulation.py +++ b/game/sim/missionsimulation.py @@ -1,5 +1,5 @@ from __future__ import annotations - +import copy import json from datetime import timedelta from pathlib import Path @@ -31,14 +31,15 @@ class MissionSimulation: self.unit_map: Optional[UnitMap] = None self.aircraft_simulation = AircraftSimulation(self.game) self.completed = False - self.time = self.game.conditions.start_time + self.time = self.game.simulation_time def begin_simulation(self) -> None: - self.time = self.game.conditions.start_time + self.time = self.game.simulation_time self.aircraft_simulation.begin_simulation() def tick(self, events: GameUpdateEvents) -> GameUpdateEvents: self.time += TICK + self.game.simulation_time = self.time if self.completed: raise RuntimeError("Simulation already completed") self.aircraft_simulation.on_game_tick(events, self.time, TICK) diff --git a/qt_ui/widgets/QConditionsWidget.py b/qt_ui/widgets/QConditionsWidget.py index 37c26d65..4c546608 100644 --- a/qt_ui/widgets/QConditionsWidget.py +++ b/qt_ui/widgets/QConditionsWidget.py @@ -65,14 +65,17 @@ class QTimeTurnWidget(QGroupBox): else: self.set_date_and_time(time) - def set_current_turn(self, turn: int, conditions: Conditions) -> None: + def set_current_turn( + self, turn: int, conditions: Conditions, time: datetime + ) -> None: """Sets the turn information display. :arg turn Current turn number. - :arg conditions Current time and weather conditions. + :arg conditions Current weather conditions. + :arg time Current time. """ self.daytime_icon.setPixmap(self.icons[conditions.time_of_day]) - self.set_date_and_time(conditions.start_time) + self.set_date_and_time(time) self.setTitle(f"Turn {turn}") def set_date_and_time(self, time: datetime) -> None: @@ -305,12 +308,15 @@ class QConditionsWidget(QFrame): self.weather_widget.hide() self.layout.addWidget(self.weather_widget, 0, 1) - def setCurrentTurn(self, turn: int, conditions: Conditions) -> None: + def setCurrentTurn( + self, turn: int, conditions: Conditions, time: datetime | None + ) -> None: """Sets the turn information display. :arg turn Current turn number. - :arg conditions Current time and weather conditions. + :arg conditions Current weather conditions. + :arg time Current time. """ - self.time_turn_widget.set_current_turn(turn, conditions) + self.time_turn_widget.set_current_turn(turn, conditions, time) self.weather_widget.setCurrentTurn(turn, conditions) self.weather_widget.show() diff --git a/qt_ui/widgets/QTopPanel.py b/qt_ui/widgets/QTopPanel.py index d2689bb9..39958eff 100644 --- a/qt_ui/widgets/QTopPanel.py +++ b/qt_ui/widgets/QTopPanel.py @@ -104,7 +104,9 @@ class QTopPanel(QFrame): if game is None: return - self.conditionsWidget.setCurrentTurn(game.turn, game.conditions) + self.conditionsWidget.setCurrentTurn( + game.turn, game.conditions, game.simulation_time + ) if game.conditions.weather.clouds: base_m = game.conditions.weather.clouds.base