Persist fast forward in save game file (#3509)

This PR allows persisting of the game state, in particular the flight
state and simulation time, after a fast forward.
This commit is contained in:
zhexu14 2025-06-14 08:16:56 +10:00 committed by GitHub
parent 81d5f82090
commit 6da9dc7a49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 37 additions and 35 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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."""

View File

@ -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(

View File

@ -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)

View File

@ -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()

View File

@ -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