From df43d2eed6f91d20db6d5dbde12a8b5a05021f4a Mon Sep 17 00:00:00 2001 From: zhexu14 <64713351+zhexu14@users.noreply.github.com> Date: Tue, 15 Oct 2024 20:10:53 +1100 Subject: [PATCH] Simplfy fast forward settings, introduce ability to skip combat instead of resolving. (#3448) This PR simplifies fast forward settings and introduces the ability to skip combat instead of resolving. --- changelog.md | 2 + game/ato/flightstate/startup.py | 6 ++- game/ato/flightstate/takeoff.py | 5 +- game/ato/flightstate/taxi.py | 5 +- game/settings/settings.py | 83 +++++++++++++++++++-------------- game/sim/aircraftsimulation.py | 21 +++++++-- game/sim/combat/aircombat.py | 9 ++++ game/sim/combat/atip.py | 2 + game/sim/combat/defendingsam.py | 7 +++ game/sim/combat/frozencombat.py | 5 +- game/sim/gameloop.py | 2 +- qt_ui/widgets/QTopPanel.py | 53 ++------------------- 12 files changed, 108 insertions(+), 92 deletions(-) diff --git a/changelog.md b/changelog.md index 46e325bb..e7627d4a 100644 --- a/changelog.md +++ b/changelog.md @@ -7,10 +7,12 @@ Saves from 11.x are not compatible with 12.0.0. * **[Engine]** Support for DCS 2.9.8.1214. * **[Campaign]** Flights are assigned different callsigns appropriate to the faction. * **[Campaign]** Removed deprecated settings for generating persistent and invulnerable AWACs and tankers. +* **[Mission Generation]** Added option to skip combat when fast forwarding, which progresses fast forward as if the combat did not occur. Simplified fast forward settings by consolidating "Fast forward mission to first contact" and "Player missions interrupt fast forward" into a single setting and expanding options for "Auto-resolve combat during fast-forward (WIP)". * **[Mods]** F/A-18 E/F/G Super Hornet mod version updated to 2.3. ## Fixes +* **[Mission Generation]** Fixed aircraft not spawning correctly on CVNs, LHAs and FARPs. * **[Campaign]** Do not allow aircraft from a captured control point to retreat if the captured control point has a damaged runway. * **[Campaign]** Do not allow ground units to be transferred to LHAs, CVNs or off map spawns. diff --git a/game/ato/flightstate/startup.py b/game/ato/flightstate/startup.py index f9f7c25d..c5542bc2 100644 --- a/game/ato/flightstate/startup.py +++ b/game/ato/flightstate/startup.py @@ -8,6 +8,9 @@ from .atdeparture import AtDeparture from .taxi import Taxi from ..starttype import StartType +from game.settings.settings import FastForwardStopCondition + + if TYPE_CHECKING: from game.ato.flight import Flight from game.settings import Settings @@ -37,7 +40,8 @@ class StartUp(AtDeparture): def should_halt_sim(self) -> bool: if ( self.flight.client_count > 0 - and self.settings.player_mission_interrupts_sim_at is StartType.COLD + and self.settings.fast_forward_stop_condition + == FastForwardStopCondition.PLAYER_STARTUP ): logging.info( f"Interrupting simulation because {self.flight} has players and has " diff --git a/game/ato/flightstate/takeoff.py b/game/ato/flightstate/takeoff.py index 119f0d99..db7a57d6 100644 --- a/game/ato/flightstate/takeoff.py +++ b/game/ato/flightstate/takeoff.py @@ -9,6 +9,8 @@ from .navigating import Navigating from ..starttype import StartType from ...utils import LBS_TO_KG +from game.settings.settings import FastForwardStopCondition + if TYPE_CHECKING: from game.ato.flight import Flight from game.settings import Settings @@ -45,7 +47,8 @@ class Takeoff(AtDeparture): def should_halt_sim(self) -> bool: if ( self.flight.client_count > 0 - and self.settings.player_mission_interrupts_sim_at is StartType.RUNWAY + and self.settings.fast_forward_stop_condition + == FastForwardStopCondition.PLAYER_TAKEOFF ): logging.info( f"Interrupting simulation because {self.flight} has players and has " diff --git a/game/ato/flightstate/taxi.py b/game/ato/flightstate/taxi.py index 96f7a430..4e0d0b88 100644 --- a/game/ato/flightstate/taxi.py +++ b/game/ato/flightstate/taxi.py @@ -8,6 +8,8 @@ from .atdeparture import AtDeparture from .takeoff import Takeoff from ..starttype import StartType +from game.settings.settings import FastForwardStopCondition + if TYPE_CHECKING: from game.ato.flight import Flight from game.settings import Settings @@ -37,7 +39,8 @@ class Taxi(AtDeparture): def should_halt_sim(self) -> bool: if ( self.flight.client_count > 0 - and self.settings.player_mission_interrupts_sim_at is StartType.WARM + and self.settings.fast_forward_stop_condition + == FastForwardStopCondition.PLAYER_TAXI ): logging.info( f"Interrupting simulation because {self.flight} has players and has " diff --git a/game/settings/settings.py b/game/settings/settings.py index 95231eb6..27c620b5 100644 --- a/game/settings/settings.py +++ b/game/settings/settings.py @@ -28,6 +28,23 @@ class AutoAtoBehavior(Enum): Prefer = "Prefer player pilots" +@unique +class FastForwardStopCondition(Enum): + DISABLED = "Fast forward disabled" + FIRST_CONTACT = "First contact" + PLAYER_TAKEOFF = "Player takeoff time" + PLAYER_TAXI = "Player taxi time" + PLAYER_STARTUP = "Player startup time" + MANUAL = "Manual fast forward control" + + +@unique +class CombatResolutionMethod(Enum): + PAUSE = "Pause simulation" + RESOLVE = "Resolve combat" + SKIP = "Skip combat" + + DIFFICULTY_PAGE = "Difficulty" AI_DIFFICULTY_SECTION = "AI Difficulty" @@ -293,7 +310,7 @@ class Settings: "Tactical commander", page=MISSION_GENERATOR_PAGE, section=COMMANDERS_SECTION, - default=1, + default=0, min=0, max=100, ) @@ -309,7 +326,7 @@ class Settings: "Observer", page=MISSION_GENERATOR_PAGE, section=COMMANDERS_SECTION, - default=1, + default=0, min=0, max=100, ) @@ -327,19 +344,6 @@ class Settings: "run out of fuel when players would not." ), ) - fast_forward_to_first_contact: bool = boolean_option( - "Fast forward mission to first contact (WIP)", - page=MISSION_GENERATOR_PAGE, - section=GAMEPLAY_SECTION, - default=False, - detail=( - "If enabled, the mission will be generated at the point of first contact. " - "If you enable this option, you will not be able to create new flights " - 'after pressing "TAKE OFF". Doing so will create an error the next time ' - 'you press "TAKE OFF". Save your game first if you want to make ' - "modifications." - ), - ) reload_pre_sim_checkpoint_on_abort: bool = boolean_option( "Reset mission to pre-take off conditions on abort", page=MISSION_GENERATOR_PAGE, @@ -351,37 +355,44 @@ class Settings: "your game after aborting take off." ), ) - player_mission_interrupts_sim_at: Optional[StartType] = choices_option( - "Player missions interrupt fast forward", + fast_forward_stop_condition: FastForwardStopCondition = choices_option( + "Fast forward until", page=MISSION_GENERATOR_PAGE, section=GAMEPLAY_SECTION, - default=None, + default=FastForwardStopCondition.DISABLED, choices={ - "Never": None, - "At startup time": StartType.COLD, - "At taxi time": StartType.WARM, - "At takeoff time": StartType.RUNWAY, + "No fast forward": FastForwardStopCondition.DISABLED, + "Player startup time": FastForwardStopCondition.PLAYER_STARTUP, + "Player taxi time": FastForwardStopCondition.PLAYER_TAXI, + "Player takeoff time": FastForwardStopCondition.PLAYER_TAKEOFF, + "First contact": FastForwardStopCondition.FIRST_CONTACT, + "Manual": FastForwardStopCondition.MANUAL, }, detail=( - "Determines what player mission states will interrupt fast-forwarding to " - "first contact, if enabled. If never is selected player missions will not " - "impact simulation and player missions may be generated mid-flight. The " - "other options will cause the mission to be generated as soon as a player " - "mission reaches the set state or at first contact, whichever comes first." + "Determines when fast forwarding stops: " + "No fast forward: disables fast forward. " + "Player startup time: fast forward until player startup time. " + "Player taxi time: fast forward until player taxi time. " + "Player takeoff time: fast forward until player takeoff time. " + "First contact: fast forward until first contact between blue and red units. " + "Manual: manually control fast forward. Show manual controls with --show-sim-speed-controls." ), ) - auto_resolve_combat: bool = boolean_option( - "Auto-resolve combat during fast-forward (WIP)", + combat_resolution_method: CombatResolutionMethod = choices_option( + "Resolve combat when fast forwarding by", page=MISSION_GENERATOR_PAGE, section=GAMEPLAY_SECTION, - default=False, + default=CombatResolutionMethod.PAUSE, + choices={ + "Pause": CombatResolutionMethod.PAUSE, + "Resolving combat (WIP)": CombatResolutionMethod.RESOLVE, + "Skipping combat": CombatResolutionMethod.SKIP, + }, detail=( - 'Requires a "Player missions interrupt fast forward" setting other than ' - '"Never" If enabled, aircraft entering combat during fast forward will have' - "their combat auto-resolved after a period of time. This allows the " - "simulation to advance further into the mission before requiring mission " - "generation, but simulation is currently very rudimentary so may result in " - "huge losses." + "Determines what happens when combat occurs when fast forwarding. " + "Pause: pause fast forward and generate mission. Fast forwarding may stop before the condition specified in the above setting. " + "Resolving combat (WIP): auto resolve combat. This method is very rudimentary and will result in large losses. " + "Skipping combat: skip combat as if it did not occur." ), ) supercarrier: bool = boolean_option( diff --git a/game/sim/aircraftsimulation.py b/game/sim/aircraftsimulation.py index 89264148..82f62200 100644 --- a/game/sim/aircraftsimulation.py +++ b/game/sim/aircraftsimulation.py @@ -10,6 +10,7 @@ from typing_extensions import TYPE_CHECKING from game.ato.flightstate import ( Uninitialized, ) +from game.settings.settings import FastForwardStopCondition, CombatResolutionMethod from .combat import CombatInitiator, FrozenCombat from .gameupdateevents import GameUpdateEvents from .simulationresults import SimulationResults @@ -32,7 +33,7 @@ class AircraftSimulation: def on_game_tick( self, events: GameUpdateEvents, time: datetime, duration: timedelta ) -> None: - if not self.game.settings.auto_resolve_combat and self.combats: + if not self._auto_resolve_combat() and self.combats: logging.error( "Cannot resume simulation because aircraft are in combat and " "auto-resolve is disabled" @@ -42,7 +43,13 @@ class AircraftSimulation: still_active = [] for combat in self.combats: - if combat.on_game_tick(time, duration, self.results, events): + if combat.on_game_tick( + time, + duration, + self.results, + events, + self.game.settings.combat_resolution_method, + ): events.end_combat(combat) else: still_active.append(combat) @@ -61,7 +68,7 @@ class AircraftSimulation: events.complete_simulation() return - if not self.game.settings.auto_resolve_combat and self.combats: + if not self._auto_resolve_combat() and self.combats: events.complete_simulation() def set_initial_flight_states(self) -> None: @@ -80,3 +87,11 @@ class AircraftSimulation: ) for package in packages: yield from package.flights + + def _auto_resolve_combat(self) -> bool: + return ( + self.game.settings.fast_forward_stop_condition + != FastForwardStopCondition.DISABLED + and self.game.settings.combat_resolution_method + != CombatResolutionMethod.PAUSE + ) diff --git a/game/sim/combat/aircombat.py b/game/sim/combat/aircombat.py index 0fc6e948..2ee7acd1 100644 --- a/game/sim/combat/aircombat.py +++ b/game/sim/combat/aircombat.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING from shapely.ops import unary_union from game.ato.flightstate import InCombat, InFlight +from game.settings.settings import CombatResolutionMethod from game.utils import dcs_to_shapely_point from .joinablecombat import JoinableCombat from .. import GameUpdateEvents @@ -67,7 +68,15 @@ class AirCombat(JoinableCombat): events: GameUpdateEvents, time: datetime, elapsed_time: timedelta, + resolution_method: CombatResolutionMethod, ) -> None: + + if resolution_method is CombatResolutionMethod.SKIP: + for flight in self.flights: + assert isinstance(flight.state, InCombat) + flight.state.exit_combat(events, time, elapsed_time) + return + blue = [] red = [] for flight in self.flights: diff --git a/game/sim/combat/atip.py b/game/sim/combat/atip.py index 61ed55b5..3de611fa 100644 --- a/game/sim/combat/atip.py +++ b/game/sim/combat/atip.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING from .frozencombat import FrozenCombat from .. import GameUpdateEvents from ...ato.flightstate import InCombat +from game.settings.settings import CombatResolutionMethod if TYPE_CHECKING: from game.ato import Flight @@ -34,6 +35,7 @@ class AtIp(FrozenCombat): events: GameUpdateEvents, time: datetime, elapsed_time: timedelta, + resolution_method: CombatResolutionMethod, ) -> None: logging.debug( f"{self.flight} attack on {self.flight.package.target} auto-resolved with " diff --git a/game/sim/combat/defendingsam.py b/game/sim/combat/defendingsam.py index 2dfe98fb..756cc665 100644 --- a/game/sim/combat/defendingsam.py +++ b/game/sim/combat/defendingsam.py @@ -7,6 +7,7 @@ from datetime import datetime, timedelta from typing import TYPE_CHECKING from game.ato.flightstate import InCombat +from game.settings.settings import CombatResolutionMethod from .frozencombat import FrozenCombat from .. import GameUpdateEvents @@ -43,8 +44,14 @@ class DefendingSam(FrozenCombat): events: GameUpdateEvents, time: datetime, elapsed_time: timedelta, + resolution_method: CombatResolutionMethod, ) -> None: assert isinstance(self.flight.state, InCombat) + + if resolution_method is CombatResolutionMethod.SKIP: + self.flight.state.exit_combat(events, time, elapsed_time) + return + if random.random() >= 0.5: logging.debug(f"Air defense combat auto-resolved with {self.flight} lost") self.flight.kill(results, events) diff --git a/game/sim/combat/frozencombat.py b/game/sim/combat/frozencombat.py index 45be3a0b..0a9af4a9 100644 --- a/game/sim/combat/frozencombat.py +++ b/game/sim/combat/frozencombat.py @@ -7,6 +7,7 @@ from datetime import datetime, timedelta from typing import TYPE_CHECKING from game.ato.flightstate import InCombat, InFlight +from game.settings.settings import CombatResolutionMethod from .. import GameUpdateEvents if TYPE_CHECKING: @@ -26,10 +27,11 @@ class FrozenCombat(ABC): duration: timedelta, results: SimulationResults, events: GameUpdateEvents, + resolution_method: CombatResolutionMethod, ) -> bool: self.elapsed_time += duration if self.elapsed_time >= self.freeze_duration: - self.resolve(results, events, time, self.elapsed_time) + self.resolve(results, events, time, self.elapsed_time, resolution_method) return True return False @@ -40,6 +42,7 @@ class FrozenCombat(ABC): events: GameUpdateEvents, time: datetime, elapsed_time: timedelta, + resolution_method: CombatResolutionMethod, ) -> None: ... @abstractmethod diff --git a/game/sim/gameloop.py b/game/sim/gameloop.py index a1588665..1b39b0b7 100644 --- a/game/sim/gameloop.py +++ b/game/sim/gameloop.py @@ -65,7 +65,7 @@ class GameLoop: self.start() logging.info("Running sim to first contact") while not self.completed: - self.tick(suppress_events=True) + self.tick(suppress_events=False) def pause_and_generate_miz(self, output: Path) -> None: self.pause() diff --git a/qt_ui/widgets/QTopPanel.py b/qt_ui/widgets/QTopPanel.py index 59942255..d2689bb9 100644 --- a/qt_ui/widgets/QTopPanel.py +++ b/qt_ui/widgets/QTopPanel.py @@ -16,6 +16,7 @@ from game import Game, persistence from game.ato.package import Package from game.ato.traveltime import TotEstimator from game.profiling import logged_duration +from game.settings.settings import FastForwardStopCondition from game.utils import meters from qt_ui.models import GameModel from qt_ui.simcontroller import SimController @@ -248,50 +249,6 @@ class QTopPanel(QFrame): mbox.exec_() return True - def check_valid_autoresolve_settings(self) -> bool: - if not self.game.settings.fast_forward_to_first_contact: - return True - - if not self.game.settings.auto_resolve_combat: - return True - - has_clients = self.ato_has_clients() - if ( - has_clients - and self.game.settings.player_mission_interrupts_sim_at is not None - ): - return True - - if has_clients: - message = textwrap.dedent( - """\ - You have enabled settings to fast forward and to auto-resolve combat, - but have not selected any interrupt condition. Fast forward will never - stop with your current settings. To use auto- resolve, you must choose a - "Player missions interrupt fast forward" setting other than "Never". - """ - ) - else: - message = textwrap.dedent( - """\ - You have enabled settings to fast forward and to auto-resolve combat, - but have no players. Fast forward will never stop with your current - settings. Auto-resolve and fast forward cannot be used without player - flights and a "Player missions interrupt fast forward" setting other - than "Never". - """ - ) - - mbox = QMessageBox( - QMessageBox.Icon.Critical, - "Incompatible fast-forward settings", - message, - parent=self, - ) - mbox.setEscapeButton(mbox.addButton(QMessageBox.StandardButton.Close)) - mbox.exec() - return False - def launch_mission(self): """Finishes planning and waits for mission completion.""" if not self.ato_has_clients() and not self.confirm_no_client_launch(): @@ -307,10 +264,10 @@ class QTopPanel(QFrame): if not self.confirm_negative_start_time(negative_starts): return - if not self.check_valid_autoresolve_settings(): - return - - if self.game.settings.fast_forward_to_first_contact: + if self.game.settings.fast_forward_stop_condition not in [ + FastForwardStopCondition.DISABLED, + FastForwardStopCondition.MANUAL, + ]: with logged_duration("Simulating to first contact"): self.sim_controller.run_to_first_contact() self.sim_controller.generate_miz(