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.
This commit is contained in:
zhexu14 2024-10-15 20:10:53 +11:00 committed by GitHub
parent 5d0ddea753
commit df43d2eed6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 108 additions and 92 deletions

View File

@ -7,10 +7,12 @@ Saves from 11.x are not compatible with 12.0.0.
* **[Engine]** Support for DCS 2.9.8.1214. * **[Engine]** Support for DCS 2.9.8.1214.
* **[Campaign]** Flights are assigned different callsigns appropriate to the faction. * **[Campaign]** Flights are assigned different callsigns appropriate to the faction.
* **[Campaign]** Removed deprecated settings for generating persistent and invulnerable AWACs and tankers. * **[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. * **[Mods]** F/A-18 E/F/G Super Hornet mod version updated to 2.3.
## Fixes ## 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 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. * **[Campaign]** Do not allow ground units to be transferred to LHAs, CVNs or off map spawns.

View File

@ -8,6 +8,9 @@ from .atdeparture import AtDeparture
from .taxi import Taxi from .taxi import Taxi
from ..starttype import StartType from ..starttype import StartType
from game.settings.settings import FastForwardStopCondition
if TYPE_CHECKING: if TYPE_CHECKING:
from game.ato.flight import Flight from game.ato.flight import Flight
from game.settings import Settings from game.settings import Settings
@ -37,7 +40,8 @@ class StartUp(AtDeparture):
def should_halt_sim(self) -> bool: def should_halt_sim(self) -> bool:
if ( if (
self.flight.client_count > 0 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( logging.info(
f"Interrupting simulation because {self.flight} has players and has " f"Interrupting simulation because {self.flight} has players and has "

View File

@ -9,6 +9,8 @@ from .navigating import Navigating
from ..starttype import StartType from ..starttype import StartType
from ...utils import LBS_TO_KG from ...utils import LBS_TO_KG
from game.settings.settings import FastForwardStopCondition
if TYPE_CHECKING: if TYPE_CHECKING:
from game.ato.flight import Flight from game.ato.flight import Flight
from game.settings import Settings from game.settings import Settings
@ -45,7 +47,8 @@ class Takeoff(AtDeparture):
def should_halt_sim(self) -> bool: def should_halt_sim(self) -> bool:
if ( if (
self.flight.client_count > 0 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( logging.info(
f"Interrupting simulation because {self.flight} has players and has " f"Interrupting simulation because {self.flight} has players and has "

View File

@ -8,6 +8,8 @@ from .atdeparture import AtDeparture
from .takeoff import Takeoff from .takeoff import Takeoff
from ..starttype import StartType from ..starttype import StartType
from game.settings.settings import FastForwardStopCondition
if TYPE_CHECKING: if TYPE_CHECKING:
from game.ato.flight import Flight from game.ato.flight import Flight
from game.settings import Settings from game.settings import Settings
@ -37,7 +39,8 @@ class Taxi(AtDeparture):
def should_halt_sim(self) -> bool: def should_halt_sim(self) -> bool:
if ( if (
self.flight.client_count > 0 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( logging.info(
f"Interrupting simulation because {self.flight} has players and has " f"Interrupting simulation because {self.flight} has players and has "

View File

@ -28,6 +28,23 @@ class AutoAtoBehavior(Enum):
Prefer = "Prefer player pilots" 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" DIFFICULTY_PAGE = "Difficulty"
AI_DIFFICULTY_SECTION = "AI Difficulty" AI_DIFFICULTY_SECTION = "AI Difficulty"
@ -293,7 +310,7 @@ class Settings:
"Tactical commander", "Tactical commander",
page=MISSION_GENERATOR_PAGE, page=MISSION_GENERATOR_PAGE,
section=COMMANDERS_SECTION, section=COMMANDERS_SECTION,
default=1, default=0,
min=0, min=0,
max=100, max=100,
) )
@ -309,7 +326,7 @@ class Settings:
"Observer", "Observer",
page=MISSION_GENERATOR_PAGE, page=MISSION_GENERATOR_PAGE,
section=COMMANDERS_SECTION, section=COMMANDERS_SECTION,
default=1, default=0,
min=0, min=0,
max=100, max=100,
) )
@ -327,19 +344,6 @@ class Settings:
"run out of fuel when players would not." "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( reload_pre_sim_checkpoint_on_abort: bool = boolean_option(
"Reset mission to pre-take off conditions on abort", "Reset mission to pre-take off conditions on abort",
page=MISSION_GENERATOR_PAGE, page=MISSION_GENERATOR_PAGE,
@ -351,37 +355,44 @@ class Settings:
"your game after aborting take off." "your game after aborting take off."
), ),
) )
player_mission_interrupts_sim_at: Optional[StartType] = choices_option( fast_forward_stop_condition: FastForwardStopCondition = choices_option(
"Player missions interrupt fast forward", "Fast forward until",
page=MISSION_GENERATOR_PAGE, page=MISSION_GENERATOR_PAGE,
section=GAMEPLAY_SECTION, section=GAMEPLAY_SECTION,
default=None, default=FastForwardStopCondition.DISABLED,
choices={ choices={
"Never": None, "No fast forward": FastForwardStopCondition.DISABLED,
"At startup time": StartType.COLD, "Player startup time": FastForwardStopCondition.PLAYER_STARTUP,
"At taxi time": StartType.WARM, "Player taxi time": FastForwardStopCondition.PLAYER_TAXI,
"At takeoff time": StartType.RUNWAY, "Player takeoff time": FastForwardStopCondition.PLAYER_TAKEOFF,
"First contact": FastForwardStopCondition.FIRST_CONTACT,
"Manual": FastForwardStopCondition.MANUAL,
}, },
detail=( detail=(
"Determines what player mission states will interrupt fast-forwarding to " "Determines when fast forwarding stops: "
"first contact, if enabled. If never is selected player missions will not " "No fast forward: disables fast forward. "
"impact simulation and player missions may be generated mid-flight. The " "Player startup time: fast forward until player startup time. "
"other options will cause the mission to be generated as soon as a player " "Player taxi time: fast forward until player taxi time. "
"mission reaches the set state or at first contact, whichever comes first." "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( combat_resolution_method: CombatResolutionMethod = choices_option(
"Auto-resolve combat during fast-forward (WIP)", "Resolve combat when fast forwarding by",
page=MISSION_GENERATOR_PAGE, page=MISSION_GENERATOR_PAGE,
section=GAMEPLAY_SECTION, section=GAMEPLAY_SECTION,
default=False, default=CombatResolutionMethod.PAUSE,
choices={
"Pause": CombatResolutionMethod.PAUSE,
"Resolving combat (WIP)": CombatResolutionMethod.RESOLVE,
"Skipping combat": CombatResolutionMethod.SKIP,
},
detail=( detail=(
'Requires a "Player missions interrupt fast forward" setting other than ' "Determines what happens when combat occurs when fast forwarding. "
'"Never" If enabled, aircraft entering combat during fast forward will have' "Pause: pause fast forward and generate mission. Fast forwarding may stop before the condition specified in the above setting. "
"their combat auto-resolved after a period of time. This allows the " "Resolving combat (WIP): auto resolve combat. This method is very rudimentary and will result in large losses. "
"simulation to advance further into the mission before requiring mission " "Skipping combat: skip combat as if it did not occur."
"generation, but simulation is currently very rudimentary so may result in "
"huge losses."
), ),
) )
supercarrier: bool = boolean_option( supercarrier: bool = boolean_option(

View File

@ -10,6 +10,7 @@ from typing_extensions import TYPE_CHECKING
from game.ato.flightstate import ( from game.ato.flightstate import (
Uninitialized, Uninitialized,
) )
from game.settings.settings import FastForwardStopCondition, CombatResolutionMethod
from .combat import CombatInitiator, FrozenCombat from .combat import CombatInitiator, FrozenCombat
from .gameupdateevents import GameUpdateEvents from .gameupdateevents import GameUpdateEvents
from .simulationresults import SimulationResults from .simulationresults import SimulationResults
@ -32,7 +33,7 @@ class AircraftSimulation:
def on_game_tick( def on_game_tick(
self, events: GameUpdateEvents, time: datetime, duration: timedelta self, events: GameUpdateEvents, time: datetime, duration: timedelta
) -> None: ) -> None:
if not self.game.settings.auto_resolve_combat and self.combats: if not self._auto_resolve_combat() and self.combats:
logging.error( logging.error(
"Cannot resume simulation because aircraft are in combat and " "Cannot resume simulation because aircraft are in combat and "
"auto-resolve is disabled" "auto-resolve is disabled"
@ -42,7 +43,13 @@ class AircraftSimulation:
still_active = [] still_active = []
for combat in self.combats: 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) events.end_combat(combat)
else: else:
still_active.append(combat) still_active.append(combat)
@ -61,7 +68,7 @@ class AircraftSimulation:
events.complete_simulation() events.complete_simulation()
return return
if not self.game.settings.auto_resolve_combat and self.combats: if not self._auto_resolve_combat() and self.combats:
events.complete_simulation() events.complete_simulation()
def set_initial_flight_states(self) -> None: def set_initial_flight_states(self) -> None:
@ -80,3 +87,11 @@ class AircraftSimulation:
) )
for package in packages: for package in packages:
yield from package.flights 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
)

View File

@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
from shapely.ops import unary_union from shapely.ops import unary_union
from game.ato.flightstate import InCombat, InFlight from game.ato.flightstate import InCombat, InFlight
from game.settings.settings import CombatResolutionMethod
from game.utils import dcs_to_shapely_point from game.utils import dcs_to_shapely_point
from .joinablecombat import JoinableCombat from .joinablecombat import JoinableCombat
from .. import GameUpdateEvents from .. import GameUpdateEvents
@ -67,7 +68,15 @@ class AirCombat(JoinableCombat):
events: GameUpdateEvents, events: GameUpdateEvents,
time: datetime, time: datetime,
elapsed_time: timedelta, elapsed_time: timedelta,
resolution_method: CombatResolutionMethod,
) -> None: ) -> 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 = [] blue = []
red = [] red = []
for flight in self.flights: for flight in self.flights:

View File

@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
from .frozencombat import FrozenCombat from .frozencombat import FrozenCombat
from .. import GameUpdateEvents from .. import GameUpdateEvents
from ...ato.flightstate import InCombat from ...ato.flightstate import InCombat
from game.settings.settings import CombatResolutionMethod
if TYPE_CHECKING: if TYPE_CHECKING:
from game.ato import Flight from game.ato import Flight
@ -34,6 +35,7 @@ class AtIp(FrozenCombat):
events: GameUpdateEvents, events: GameUpdateEvents,
time: datetime, time: datetime,
elapsed_time: timedelta, elapsed_time: timedelta,
resolution_method: CombatResolutionMethod,
) -> None: ) -> None:
logging.debug( logging.debug(
f"{self.flight} attack on {self.flight.package.target} auto-resolved with " f"{self.flight} attack on {self.flight.package.target} auto-resolved with "

View File

@ -7,6 +7,7 @@ from datetime import datetime, timedelta
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from game.ato.flightstate import InCombat from game.ato.flightstate import InCombat
from game.settings.settings import CombatResolutionMethod
from .frozencombat import FrozenCombat from .frozencombat import FrozenCombat
from .. import GameUpdateEvents from .. import GameUpdateEvents
@ -43,8 +44,14 @@ class DefendingSam(FrozenCombat):
events: GameUpdateEvents, events: GameUpdateEvents,
time: datetime, time: datetime,
elapsed_time: timedelta, elapsed_time: timedelta,
resolution_method: CombatResolutionMethod,
) -> None: ) -> None:
assert isinstance(self.flight.state, InCombat) 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: if random.random() >= 0.5:
logging.debug(f"Air defense combat auto-resolved with {self.flight} lost") logging.debug(f"Air defense combat auto-resolved with {self.flight} lost")
self.flight.kill(results, events) self.flight.kill(results, events)

View File

@ -7,6 +7,7 @@ from datetime import datetime, timedelta
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from game.ato.flightstate import InCombat, InFlight from game.ato.flightstate import InCombat, InFlight
from game.settings.settings import CombatResolutionMethod
from .. import GameUpdateEvents from .. import GameUpdateEvents
if TYPE_CHECKING: if TYPE_CHECKING:
@ -26,10 +27,11 @@ class FrozenCombat(ABC):
duration: timedelta, duration: timedelta,
results: SimulationResults, results: SimulationResults,
events: GameUpdateEvents, events: GameUpdateEvents,
resolution_method: CombatResolutionMethod,
) -> bool: ) -> bool:
self.elapsed_time += duration self.elapsed_time += duration
if self.elapsed_time >= self.freeze_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 True
return False return False
@ -40,6 +42,7 @@ class FrozenCombat(ABC):
events: GameUpdateEvents, events: GameUpdateEvents,
time: datetime, time: datetime,
elapsed_time: timedelta, elapsed_time: timedelta,
resolution_method: CombatResolutionMethod,
) -> None: ... ) -> None: ...
@abstractmethod @abstractmethod

View File

@ -65,7 +65,7 @@ class GameLoop:
self.start() self.start()
logging.info("Running sim to first contact") logging.info("Running sim to first contact")
while not self.completed: while not self.completed:
self.tick(suppress_events=True) self.tick(suppress_events=False)
def pause_and_generate_miz(self, output: Path) -> None: def pause_and_generate_miz(self, output: Path) -> None:
self.pause() self.pause()

View File

@ -16,6 +16,7 @@ from game import Game, persistence
from game.ato.package import Package from game.ato.package import Package
from game.ato.traveltime import TotEstimator from game.ato.traveltime import TotEstimator
from game.profiling import logged_duration from game.profiling import logged_duration
from game.settings.settings import FastForwardStopCondition
from game.utils import meters from game.utils import meters
from qt_ui.models import GameModel from qt_ui.models import GameModel
from qt_ui.simcontroller import SimController from qt_ui.simcontroller import SimController
@ -248,50 +249,6 @@ class QTopPanel(QFrame):
mbox.exec_() mbox.exec_()
return True 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): def launch_mission(self):
"""Finishes planning and waits for mission completion.""" """Finishes planning and waits for mission completion."""
if not self.ato_has_clients() and not self.confirm_no_client_launch(): 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): if not self.confirm_negative_start_time(negative_starts):
return return
if not self.check_valid_autoresolve_settings(): if self.game.settings.fast_forward_stop_condition not in [
return FastForwardStopCondition.DISABLED,
FastForwardStopCondition.MANUAL,
if self.game.settings.fast_forward_to_first_contact: ]:
with logged_duration("Simulating to first contact"): with logged_duration("Simulating to first contact"):
self.sim_controller.run_to_first_contact() self.sim_controller.run_to_first_contact()
self.sim_controller.generate_miz( self.sim_controller.generate_miz(