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.
* **[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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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