mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Checkpoint game before sim, auto-revert on abort.
An alternative to https://github.com/dcs-liberation/dcs_liberation/pull/2891 that I ended up liking much better (I had assumed some part of the UI would fail or at least look terrible with this approach, but it seems to work quite well). On by default now since it's far less frightening than the previous thing. Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2735.
This commit is contained in:
parent
f10350dac4
commit
24e72475b4
@ -24,7 +24,7 @@ Saves from 6.x are not compatible with 7.0.
|
||||
* **[Mission Generation]** Wind speeds no longer follow a uniform distribution. Median wind speeds are now much lower and the standard deviation has been reduced considerably at altitude but increased somewhat at MSL.
|
||||
* **[Mission Generation]** Improved task generation for SEAD flights carrying TALDs.
|
||||
* **[Mission Generation]** Added task timeout for SEAD flights with TALDs to prevent AI from overflying the target.
|
||||
* **[Mission Generation]** Added (experimental!) option to rewind when aborting take off. This will allow you to modify your ATO after clicking take off without having to reload your game.
|
||||
* **[Mission Generation]** Game state will automatically be checkpointed before fast-forwarding the mission, and restored on mission abort. This means that it's now possible to abort a mission and make changes without needing to manually re-load your game.
|
||||
* **[Modding]** Updated Community A-4E-C mod version support to 2.1.0 release.
|
||||
* **[Modding]** Add support for VSN F-4B and F-4C mod.
|
||||
* **[Modding]** Added support for AI C-47 mod.
|
||||
|
||||
@ -25,6 +25,7 @@ class SaveGameBundle:
|
||||
MANUAL_SAVE_NAME = "player.liberation"
|
||||
LAST_TURN_SAVE_NAME = "last_turn.liberation"
|
||||
START_OF_TURN_SAVE_NAME = "start_of_turn.liberation"
|
||||
PRE_SIM_CHECKPOINT_SAVE_NAME = "pre_sim_checkpoint.liberation"
|
||||
|
||||
def __init__(self, bundle_path: Path) -> None:
|
||||
self.bundle_path = bundle_path
|
||||
@ -58,6 +59,19 @@ class SaveGameBundle:
|
||||
game, self.START_OF_TURN_SAVE_NAME, copy_from=self
|
||||
)
|
||||
|
||||
def save_pre_sim_checkpoint(self, game: Game) -> None:
|
||||
"""Writes the save file for the state before beginning simulation.
|
||||
|
||||
This save is the state of the game after the player presses "TAKE OFF", but
|
||||
before the fast-forward simulation begins. It is not practical to rewind, but
|
||||
players commonly will want to cancel and continue planning after pressing that
|
||||
button, so we make a checkpoint that we can reload on abort.
|
||||
"""
|
||||
with logged_duration("Saving pre-sim checkpoint"):
|
||||
self._update_bundle_member(
|
||||
game, self.PRE_SIM_CHECKPOINT_SAVE_NAME, copy_from=self
|
||||
)
|
||||
|
||||
def load_player(self) -> Game:
|
||||
"""Loads the save manually created by the player via save/save-as."""
|
||||
return self._load_from(self.MANUAL_SAVE_NAME)
|
||||
@ -70,6 +84,10 @@ class SaveGameBundle:
|
||||
"""Loads the save automatically created at the end of the last turn."""
|
||||
return self._load_from(self.LAST_TURN_SAVE_NAME)
|
||||
|
||||
def load_pre_sim_checkpoint(self) -> Game:
|
||||
"""Loads the save automatically created before the simulation began."""
|
||||
return self._load_from(self.PRE_SIM_CHECKPOINT_SAVE_NAME)
|
||||
|
||||
def _load_from(self, name: str) -> Game:
|
||||
with ZipFile(self.bundle_path) as zip_bundle:
|
||||
with zip_bundle.open(name, "r") as save:
|
||||
|
||||
@ -51,6 +51,10 @@ class SaveManager:
|
||||
with self._save_bundle_context() as bundle:
|
||||
bundle.save_start_of_turn(self.game)
|
||||
|
||||
def save_pre_sim_checkpoint(self) -> None:
|
||||
with self._save_bundle_context() as bundle:
|
||||
bundle.save_pre_sim_checkpoint(self.game)
|
||||
|
||||
def set_loaded_from(self, bundle: SaveGameBundle) -> None:
|
||||
"""Reconfigures this save manager based on the loaded game.
|
||||
|
||||
@ -81,6 +85,9 @@ class SaveManager:
|
||||
self.last_saved_bundle = previous_saved_bundle
|
||||
raise
|
||||
|
||||
def load_pre_sim_checkpoint(self) -> Game:
|
||||
return self.default_save_bundle.load_pre_sim_checkpoint()
|
||||
|
||||
@staticmethod
|
||||
def load_last_turn(bundle_path: Path) -> Game:
|
||||
return SaveGameBundle(bundle_path).load_last_turn()
|
||||
|
||||
@ -324,17 +324,15 @@ class Settings:
|
||||
"modifications."
|
||||
),
|
||||
)
|
||||
reset_simulation_on_abort: bool = boolean_option(
|
||||
"Reset mission to pre-take off conditions on abort (experimental)",
|
||||
reload_pre_sim_checkpoint_on_abort: bool = boolean_option(
|
||||
"Reset mission to pre-take off conditions on abort",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
section=GAMEPLAY_SECTION,
|
||||
default=False,
|
||||
default=True,
|
||||
detail=(
|
||||
"If enabled, the fast-forward effects will be rewound when aborting take "
|
||||
"off. <strong>DO NOT USE THIS WITH AUTO-RESOLVE ENABLED.</strong> Lost "
|
||||
"aircraft will not be recovered. This option is experimental and may not "
|
||||
"work. It is always safer to reload your save after abort when using fast-"
|
||||
"forward."
|
||||
"If enabled, the game will automatically reload a pre-take off save when "
|
||||
"aborting take off. If this is disabled, you will need to manually re-load "
|
||||
"your game after aborting take off."
|
||||
),
|
||||
)
|
||||
player_mission_interrupts_sim_at: Optional[StartType] = choices_option(
|
||||
|
||||
@ -69,12 +69,9 @@ class AircraftSimulation:
|
||||
for flight in self.iter_flights():
|
||||
flight.state.reinitialize(now)
|
||||
|
||||
def reset(self, events: GameUpdateEvents | None = None) -> None:
|
||||
if events is None:
|
||||
events = GameUpdateEvents()
|
||||
def reset(self) -> None:
|
||||
for flight in self.iter_flights():
|
||||
flight.set_state(Uninitialized(flight, self.game.settings))
|
||||
events.update_flight(flight)
|
||||
self.combats = []
|
||||
|
||||
def iter_flights(self) -> Iterator[Flight]:
|
||||
|
||||
@ -36,17 +36,10 @@ class GameLoop:
|
||||
def elapsed_time(self) -> timedelta:
|
||||
return self.sim.time - self.game.conditions.start_time
|
||||
|
||||
def reset(self) -> None:
|
||||
self.pause()
|
||||
self.events = GameUpdateEvents()
|
||||
self.sim.reset(self.events)
|
||||
self.send_update(rate_limit=False)
|
||||
self.started = False
|
||||
self.completed = False
|
||||
|
||||
def start(self) -> None:
|
||||
if self.started:
|
||||
raise RuntimeError("Cannot start game loop because it has already started")
|
||||
self.game.save_manager.save_pre_sim_checkpoint()
|
||||
self.started = True
|
||||
self.sim.begin_simulation()
|
||||
|
||||
|
||||
@ -33,11 +33,6 @@ class MissionSimulation:
|
||||
self.completed = False
|
||||
self.time = self.game.conditions.start_time
|
||||
|
||||
def reset(self, events: GameUpdateEvents) -> None:
|
||||
self.completed = False
|
||||
self.time = self.game.conditions.start_time
|
||||
self.aircraft_simulation.reset(events)
|
||||
|
||||
def begin_simulation(self) -> None:
|
||||
self.time = self.game.conditions.start_time
|
||||
self.aircraft_simulation.begin_simulation()
|
||||
|
||||
@ -84,9 +84,6 @@ class SimController(QObject):
|
||||
with self.game_loop.paused_sim():
|
||||
yield
|
||||
|
||||
def reset_simulation(self) -> None:
|
||||
self.game_loop.reset()
|
||||
|
||||
def run_to_first_contact(self) -> None:
|
||||
self.game_loop.run_to_first_contact()
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Callable
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
@ -33,11 +33,16 @@ from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResul
|
||||
|
||||
class QTopPanel(QFrame):
|
||||
def __init__(
|
||||
self, game_model: GameModel, sim_controller: SimController, ui_flags: UiFlags
|
||||
self,
|
||||
game_model: GameModel,
|
||||
sim_controller: SimController,
|
||||
ui_flags: UiFlags,
|
||||
reset_to_pre_sim_checkpoint: Callable[[], None],
|
||||
) -> None:
|
||||
super(QTopPanel, self).__init__()
|
||||
self.game_model = game_model
|
||||
self.sim_controller = sim_controller
|
||||
self.reset_to_pre_sim_checkpoint = reset_to_pre_sim_checkpoint
|
||||
self.dialog: Optional[QDialog] = None
|
||||
|
||||
self.setMaximumHeight(70)
|
||||
@ -293,7 +298,9 @@ class QTopPanel(QFrame):
|
||||
persistence.mission_path_for("liberation_nextturn.miz")
|
||||
)
|
||||
|
||||
waiting = QWaitingForMissionResultWindow(self.game, self.sim_controller, self)
|
||||
waiting = QWaitingForMissionResultWindow(
|
||||
self.game, self.sim_controller, self.reset_to_pre_sim_checkpoint, self
|
||||
)
|
||||
waiting.exec_()
|
||||
|
||||
def budget_update(self, game: Game):
|
||||
|
||||
@ -133,7 +133,14 @@ class QLiberationWindow(QMainWindow):
|
||||
|
||||
vbox = QVBoxLayout()
|
||||
vbox.setContentsMargins(0, 0, 0, 0)
|
||||
vbox.addWidget(QTopPanel(self.game_model, self.sim_controller, ui_flags))
|
||||
vbox.addWidget(
|
||||
QTopPanel(
|
||||
self.game_model,
|
||||
self.sim_controller,
|
||||
ui_flags,
|
||||
self.reset_to_pre_sim_checkpoint,
|
||||
)
|
||||
)
|
||||
vbox.addWidget(hbox)
|
||||
|
||||
central_widget = QWidget()
|
||||
@ -340,6 +347,23 @@ class QLiberationWindow(QMainWindow):
|
||||
except Exception:
|
||||
logging.exception("Error loading save game %s", file[0])
|
||||
|
||||
def reset_to_pre_sim_checkpoint(self) -> None:
|
||||
"""Loads the game that was saved before pressing the take-off button.
|
||||
|
||||
A checkpoint will be saved when the player presses take-off to save their state
|
||||
before the mission simulation begins. If the mission is aborted, we usually want
|
||||
to reset to the pre-simulation state to allow players to effectively "rewind",
|
||||
since they probably aborted so that they could make changes. Implementing rewind
|
||||
for real is impractical, but checkpoints are easy.
|
||||
"""
|
||||
if self.game is None:
|
||||
raise RuntimeError(
|
||||
"Cannot reset to pre-sim checkpoint when no game is loaded"
|
||||
)
|
||||
GameUpdateSignal.get_instance().game_loaded.emit(
|
||||
self.game.save_manager.load_pre_sim_checkpoint()
|
||||
)
|
||||
|
||||
def saveGame(self):
|
||||
logging.info("Saving game")
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Optional, Callable
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
@ -52,12 +52,14 @@ class QWaitingForMissionResultWindow(QDialog):
|
||||
self,
|
||||
game: Game,
|
||||
sim_controller: SimController,
|
||||
reset_to_pre_sim_checkpoint: Callable[[], None],
|
||||
parent: Optional[QWidget] = None,
|
||||
) -> None:
|
||||
super(QWaitingForMissionResultWindow, self).__init__(parent=parent)
|
||||
self.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self.game = game
|
||||
self.sim_controller = sim_controller
|
||||
self.reset_to_pre_sim_checkpoint = reset_to_pre_sim_checkpoint
|
||||
self.setWindowTitle("Waiting for mission completion.")
|
||||
self.setWindowIcon(QIcon("./resources/icon.png"))
|
||||
self.setMinimumHeight(570)
|
||||
@ -134,8 +136,8 @@ class QWaitingForMissionResultWindow(QDialog):
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def reject(self) -> None:
|
||||
if self.game.settings.reset_simulation_on_abort:
|
||||
self.sim_controller.reset_simulation()
|
||||
if self.game.settings.reload_pre_sim_checkpoint_on_abort:
|
||||
self.reset_to_pre_sim_checkpoint()
|
||||
super().reject()
|
||||
|
||||
@staticmethod
|
||||
|
||||
@ -56,6 +56,16 @@ def test_save_start_of_turn(game: Game, tmp_bundle: SaveGameBundle) -> None:
|
||||
assert zip_file.namelist() == [SaveGameBundle.START_OF_TURN_SAVE_NAME]
|
||||
|
||||
|
||||
def test_save_pre_sim_checkpoint(game: Game, tmp_bundle: SaveGameBundle) -> None:
|
||||
with ZipFile(tmp_bundle.bundle_path, "r") as zip_file:
|
||||
with pytest.raises(KeyError):
|
||||
zip_file.read(SaveGameBundle.PRE_SIM_CHECKPOINT_SAVE_NAME)
|
||||
tmp_bundle.save_pre_sim_checkpoint(game)
|
||||
|
||||
with ZipFile(tmp_bundle.bundle_path, "r") as zip_file:
|
||||
assert zip_file.namelist() == [SaveGameBundle.PRE_SIM_CHECKPOINT_SAVE_NAME]
|
||||
|
||||
|
||||
def test_failed_save_leaves_original_intact(
|
||||
game: Game, tmp_bundle: SaveGameBundle
|
||||
) -> None:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user