Add option to fast forward to first contact.

This is the first step in a larger project to add play/pause buttons to
the Liberation UI so the mission can be generated at any point.
docs/design/turnless.md describes the plan.

This adds an option to fast forward the turn to first contact before
generating the mission. None of that is reflected in the UI (for now),
but the miz will be generated with many flights in the air.

For now "first contact" means as soon as any flight reaches its IP. I'll
follow up to add threat checking so that air-to-air combat also triggers
this, as will entering a SAM's threat zone.

This also includes an option to halt fast-forward whenever a player
flight reaches a certain mission-prep phase. This can be used to avoid
fast forwarding past the player's startup time, taxi time, or takeoff
time. By default this option is disabled so player aircraft may start in
the air (possibly even at their IP if they're the first mission to reach
IP).

Fuel states do not currently account for distance traveled during fast
forward. That will come later.

https://github.com/dcs-liberation/dcs_liberation/issues/1681
This commit is contained in:
Dan Albert 2021-10-22 11:45:06 -07:00
parent c8f30b3289
commit 5db1b94ac4
38 changed files with 910 additions and 145 deletions

180
doc/design/turnless.md Normal file
View File

@ -0,0 +1,180 @@
# Turnless Campaign
This outlines a path for us converting Liberation from turn based into a
turnless campaign game like BMS.
## Hard problems
### We can't simulate combat
We can't feasibly simulate the details of combat, nor can we generate a mission
mid-engagement. The mission editor does not support generating aircraft
mid-maneuver, or with any SA.
To avoid that problem, instead of simulating the combat we put the group in a
"freeze" at the moment the engagement begins. Any frozen groups are spawned
wherever they were frozen so simulation can be deferred to DCS. Each freeze will
have a deadline for completion, at which point a simple strength-based
autoresolve will take place.
After a frozen engagement is resolved (whether in sim or automatically), if the
mission objective has not been achieved a mission abort will be evaluated.
### We don't know where the roads are
We don't know where the in-game roads are, so we can't precisely simulate the
location of convoys as they move, nor the amount of time they will take to move
between bases.
Time can be solved by just having the campaign designer encode the travel time
in the campaign. The ME will estimate it for them so its simple to do. Spawn
locations cannot be made precise, but we can have campaign designers define
several waypoints along the route and spawn the convoy at the most appropriate
one.
### SAM engagement zones are huge
The large threat zone of many SAMs would cause most of the map to be a frozen
engagement.
To avoid that, SAMs will not cause frozen engagements, but will be simulated at
a basic level. Each SAM will track a group, and when in range will periodically
“shoot”, and those shots will have a chance to kill an aircraft in the targeted
group.
## Systems that need to change
### AI Commander
Theater state tracking for the HTN becomes much more complicated since it needs
to always be able to replan based on pending plans. This shouldn't be too bad
since RndName's PR did similar and the plan to continue that should work here.
The commander needs to learn to abort missions that are no longer viable.
### Transports
With turns gone, transports can longer move one link per turn. See the entry in
"hard problems" above. To begin with we can pick an arbitrary time per link and
just always spawn the transport at the origin regardless of where they ought to
be.
### Turn based systems
A number of systems that act in a number of turns become time based:
- Runway repair
- Unit delivery
- Weather changes
- Pilot replenishment
- Income
Procurement could be made simpler if we do away with cash in favor of fixed
replenishment rates.
### Front line advancement
Front line combat is happening continuously so we need to simulate its movement
and losses on the front line.
### Mission generation
The mission generator needs to be able to generate aircraft in specific
locations, with partial fuel, and possibly with some weapons expended.
### Mission result processing
Last known positions need to be recorded, as do weapon inventory and fuel
states.
### UI
Frozen combats need to be immutable to the player.
Missions in progress need to be made cancelable.
The map needs to show the locations of in-progress missions, including frozen
engagements.
There needs to be a UI to examine frozen engagements.
## Roadmap
This roadmap is ordered so that we can attempt to make this change in phases
that may take more than a single release. It is not focused on providing the
best gameplay experience ASAP, but on being able to approach this gradually
rather than freezing all other development or branching and a painful merge.
### Front-line progress rework
Add auto-resolve for the front line, adjust progress estimates based on stances
and not just kills (an enemy in retreat is less likely to be killed, but still
gives up ground).
### Add play from first-contact option
Add simulation of the mission from the start of the turn up to the first
engagement. Generate the mission from that point.
### Add fuel expenditure estimation for travel to start location
Adjust fuel quantities at the start of the mission to account for travel.
### Add UI to display simulation to first contact
Before removing turns, add play/pause buttons to the game that will simulate the
mission up to the first engagement. Once play is pressed no changes may be made
to the ATO.
When the turn ends it behaves like the normal end of turn. The ATO is reset and
the new turn is planned.
### Add mission abort option for players
Add the option to abort flight and packages for players. The AI wont use this
yet but it will be needed once engagement simulation is added.
### Add simulation for frozen combats
Allows play to proceed through engagements. The AI will abort missions that are
unlikely to succeed after each combat is resolved.
### Estimate fuel/weapon use simulation for frozen combats
Improve behavior for groups that have completed an auto-resolved combat by
estimating their fuel and ammo use. This could be used to check if the mission
should be aborted.
### Add player options to create new packages after pressing play
Allow players to create new flights as the mission simulates. This would be the
most effective way to add intercept missions.
### Make planning real-time
Make the AI planner periodically reevaluate missions to add to the ATO. It will
still plan as many missions as possible up front so that the DCS side of the
mission can run as long as possible, but it could make use of aircraft that had
aborted or returned.
### Time-based systems instead of turn based
Move the systems that are currently based on turns to be based on duration. Each
turn is assumed to be six hours since there are four turns in a day.
### Time between turns based on mission duration
Change the turn duration to be based on the length of the mission played.
### Track end positions of aircraft, remove turns
If we can track the status of missions after the simulation ends we can continue
where we left off. Do that, and remove the concept of turns.
### Track ammo/fuel status of aircraft at the end of the mission
Improve post-simulation realism by tracking the ammunition and fuel spent.
### Add turnaround times for landed aircraft
Once aircraft returning from a mission land, a timer starts to track the
turnaround time before they'll be ready to redeploy as opposed to being instant.

View File

@ -6,6 +6,7 @@ from typing import Any, Optional, List, TYPE_CHECKING
from gen.flights.loadouts import Loadout
from .flightroster import FlightRoster
from .flightstate import Uninitialized, FlightState
if TYPE_CHECKING:
from game.dcs.aircrafttype import AircraftType
@ -52,6 +53,9 @@ class Flight:
# Only used by transport missions.
self.cargo = cargo
# Used for simulating the travel to first contact.
self.state: FlightState = Uninitialized(self, squadron.settings)
# Will be replaced with a more appropriate FlightPlan by
# FlightPlanBuilder, but an empty flight plan the flight begins with an
# empty flight plan.
@ -61,6 +65,19 @@ class Flight:
package=package, flight=self, custom_waypoints=[]
)
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 departure(self) -> ControlPoint:
return self.squadron.location
@ -113,3 +130,10 @@ class Flight:
if self.custom_name:
return f"{self.custom_name} {self.count} x {self.unit_type}"
return f"[{self.flight_type}] {self.count} x {self.unit_type}"
def set_state(self, state: FlightState) -> None:
self.state = state
def on_game_tick(self, time: datetime, duration: timedelta) -> bool:
self.state.on_game_tick(time, duration)
return self.state.should_halt_sim()

View File

@ -0,0 +1,8 @@
from .completed import Completed
from .flightstate import FlightState
from .inflight import InFlight
from .startup import StartUp
from .takeoff import Takeoff
from .taxi import Taxi
from .uninitialized import Uninitialized
from .waitingforstart import WaitingForStart

View File

@ -0,0 +1,18 @@
from datetime import datetime, timedelta
from .flightstate import FlightState
from ..starttype import StartType
class Completed(FlightState):
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
return
@property
def is_waiting_for_start(self) -> bool:
return False
@property
def spawn_type(self) -> StartType:
# TODO: May want to do something different to make these uncontrolled?
return StartType.COLD

View File

@ -0,0 +1,34 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from game.ato.starttype import StartType
if TYPE_CHECKING:
from game.ato.flight import Flight
from game.settings import Settings
class FlightState(ABC):
def __init__(self, flight: Flight, settings: Settings) -> None:
self.flight = flight
self.settings = settings
@abstractmethod
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
...
def should_halt_sim(self) -> bool:
return False
@property
@abstractmethod
def is_waiting_for_start(self) -> bool:
...
@property
@abstractmethod
def spawn_type(self) -> StartType:
...

View File

@ -0,0 +1,134 @@
from __future__ import annotations
import logging
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from dcs import Point
from game.ato.flightstate import Completed
from game.ato.flightstate.flightstate import FlightState
from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.starttype import StartType
from game.utils import Distance, Speed, meters
from gen.flights.flightplan import LoiterFlightPlan, PatrollingFlightPlan
if TYPE_CHECKING:
from game.ato.flight import Flight
from game.settings import Settings
def lerp(v0: float, v1: float, t: float) -> float:
return (1 - t) * v0 + t * v1
class InFlight(FlightState):
def __init__(self, flight: Flight, settings: Settings) -> None:
super().__init__(flight, settings)
self.waypoints = flight.flight_plan.iter_waypoints()
# TODO: Error checking for stupid flight plans with fewer than two waypoints.
self.current_waypoint = next(self.waypoints)
self.next_waypoint = next(self.waypoints)
self.passed_waypoints = {self.current_waypoint}
self.total_time_to_next_waypoint = self._total_time()
self.elapsed_time = timedelta()
def _total_time(self) -> timedelta:
if isinstance(self.flight.flight_plan, PatrollingFlightPlan):
# Racetracks should remain at the first waypoint until the patrol ends so
# that the waypoint generation doesn't need to reverse the orbit direction.
if self.current_waypoint == self.flight.flight_plan.patrol_start:
return self.flight.flight_plan.patrol_duration
# Loiter time is already built into travel_time_between_waypoints.
return self.flight.flight_plan.travel_time_between_waypoints(
self.current_waypoint, self.next_waypoint
)
def progress(self) -> float:
return (
self.elapsed_time.total_seconds()
/ self.total_time_to_next_waypoint.total_seconds()
)
def estimate_position(self) -> Point:
# TODO: Make Loiter and RaceTrack distinct FlightStates.
if isinstance(self.flight.flight_plan, PatrollingFlightPlan):
# Prevent spawning racetracks in the middle of a leg. For simplicity we
# always start the aircraft at the beginning of the racetrack.
if self.current_waypoint == self.flight.flight_plan.patrol_start:
return self.current_waypoint.position
elif isinstance(self.flight.flight_plan, LoiterFlightPlan):
if (
self.current_waypoint == self.flight.flight_plan.hold
and self.elapsed_time < self.flight.flight_plan.hold_duration
):
return self.current_waypoint.position
x0 = self.current_waypoint.position.x
y0 = self.current_waypoint.position.y
x1 = self.next_waypoint.position.x
y1 = self.next_waypoint.position.y
progress = self.progress()
return Point(lerp(x0, x1, progress), lerp(y0, y1, progress))
def estimate_altitude(self) -> tuple[Distance, str]:
# TODO: Should count progress as 0 until departing a hold.
return (
meters(
lerp(
self.current_waypoint.alt.meters,
self.next_waypoint.alt.meters,
self.progress(),
)
),
self.current_waypoint.alt_type,
)
def estimate_speed(self) -> Speed:
# TODO: Patrol/loiter speeds may be different.
return self.flight.flight_plan.speed_between_waypoints(
self.current_waypoint, self.next_waypoint
)
def update_waypoints(self) -> None:
self.current_waypoint = self.next_waypoint
self.passed_waypoints.add(self.current_waypoint)
try:
self.next_waypoint = next(self.waypoints)
except StopIteration:
self.flight.set_state(Completed(self.flight, self.settings))
self.total_time_to_next_waypoint = self._total_time()
self.elapsed_time = timedelta()
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
self.elapsed_time += duration
if self.elapsed_time > self.total_time_to_next_waypoint:
self.update_waypoints()
def should_halt_sim(self) -> bool:
contact_types = {
FlightWaypointType.INGRESS_BAI,
FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_DEAD,
FlightWaypointType.INGRESS_OCA_AIRCRAFT,
FlightWaypointType.INGRESS_OCA_RUNWAY,
FlightWaypointType.INGRESS_SEAD,
FlightWaypointType.INGRESS_STRIKE,
}
# TODO: Check against enemy threats.
if self.current_waypoint.waypoint_type in contact_types:
logging.info(
f"Interrupting simulation because {self.flight} has reached its "
"ingress point"
)
return True
return False
@property
def is_waiting_for_start(self) -> bool:
return False
@property
def spawn_type(self) -> StartType:
return StartType.IN_FLIGHT

View File

@ -0,0 +1,44 @@
from __future__ import annotations
import logging
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from .flightstate import FlightState
from .taxi import Taxi
from ..starttype import StartType
if TYPE_CHECKING:
from game.ato.flight import Flight
from game.settings import Settings
class StartUp(FlightState):
def __init__(self, flight: Flight, settings: Settings, now: datetime) -> None:
super().__init__(flight, settings)
self.completion_time = now + flight.flight_plan.estimate_startup()
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
if time < self.completion_time:
return
self.flight.set_state(Taxi(self.flight, self.settings, time))
@property
def is_waiting_for_start(self) -> bool:
return False
@property
def spawn_type(self) -> StartType:
return StartType.COLD
def should_halt_sim(self) -> bool:
if (
self.flight.client_count > 0
and self.settings.player_mission_interrupts_sim_at is StartType.COLD
):
logging.info(
f"Interrupting simulation because {self.flight} has players and has "
"reached startup time"
)
return True
return False

View File

@ -0,0 +1,45 @@
from __future__ import annotations
import logging
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from .flightstate import FlightState
from .inflight import InFlight
from ..starttype import StartType
if TYPE_CHECKING:
from game.ato.flight import Flight
from game.settings import Settings
class Takeoff(FlightState):
def __init__(self, flight: Flight, settings: Settings, now: datetime) -> None:
super().__init__(flight, settings)
# TODO: Not accounted for in FlightPlan, can cause discrepancy without loiter.
self.completion_time = now + timedelta(seconds=30)
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
if time < self.completion_time:
return
self.flight.set_state(InFlight(self.flight, self.settings))
@property
def is_waiting_for_start(self) -> bool:
return False
@property
def spawn_type(self) -> StartType:
return StartType.RUNWAY
def should_halt_sim(self) -> bool:
if (
self.flight.client_count > 0
and self.settings.player_mission_interrupts_sim_at is StartType.RUNWAY
):
logging.info(
f"Interrupting simulation because {self.flight} has players and has "
"reached takeoff time"
)
return True
return False

View File

@ -0,0 +1,44 @@
from __future__ import annotations
import logging
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from .flightstate import FlightState
from .takeoff import Takeoff
from ..starttype import StartType
if TYPE_CHECKING:
from game.ato.flight import Flight
from game.settings import Settings
class Taxi(FlightState):
def __init__(self, flight: Flight, settings: Settings, now: datetime) -> None:
super().__init__(flight, settings)
self.completion_time = now + flight.flight_plan.estimate_ground_ops()
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
if time < self.completion_time:
return
self.flight.set_state(Takeoff(self.flight, self.settings, time))
@property
def is_waiting_for_start(self) -> bool:
return False
@property
def spawn_type(self) -> StartType:
return StartType.WARM
def should_halt_sim(self) -> bool:
if (
self.flight.client_count > 0
and self.settings.player_mission_interrupts_sim_at is StartType.WARM
):
logging.info(
f"Interrupting simulation because {self.flight} has players and has "
"reached taxi time"
)
return True
return False

View File

@ -0,0 +1,17 @@
from datetime import datetime, timedelta
from .flightstate import FlightState
from ..starttype import StartType
class Uninitialized(FlightState):
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
@property
def is_waiting_for_start(self) -> bool:
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
@property
def spawn_type(self) -> StartType:
raise RuntimeError("Attempted to simulate flight that is not fully initialized")

View File

@ -0,0 +1,56 @@
from __future__ import annotations
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from game.ato.starttype import StartType
from .flightstate import FlightState
from .inflight import InFlight
from .startup import StartUp
from .takeoff import Takeoff
from .taxi import Taxi
if TYPE_CHECKING:
from game.ato.flight import Flight
from game.settings import Settings
class WaitingForStart(FlightState):
def __init__(
self,
flight: Flight,
settings: Settings,
start_time: datetime,
) -> None:
super().__init__(flight, settings)
self.start_time = start_time
@property
def start_type(self) -> StartType:
return self.flight.start_type
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
if time < self.start_time:
return
new_state: FlightState
if self.start_type is StartType.COLD:
new_state = StartUp(self.flight, self.settings, time)
elif self.start_type is StartType.WARM:
new_state = Taxi(self.flight, self.settings, time)
elif self.start_type is StartType.RUNWAY:
new_state = Takeoff(self.flight, self.settings, time)
else:
new_state = InFlight(self.flight, self.settings)
self.flight.set_state(new_state)
@property
def is_waiting_for_start(self) -> bool:
return True
def time_remaining(self, time: datetime) -> timedelta:
return self.start_time - time
@property
def spawn_type(self) -> StartType:
return self.flight.start_type

View File

@ -63,34 +63,3 @@ class FlightWaypoint:
@property
def position(self) -> Point:
return Point(self.x, self.y)
@classmethod
def from_pydcs(cls, point: MovingPoint, from_cp: ControlPoint) -> "FlightWaypoint":
waypoint = FlightWaypoint(
FlightWaypointType.NAV,
point.position.x,
point.position.y,
meters(point.alt),
)
waypoint.alt_type = point.alt_type
# Other actions exist... but none of them *should* be the first
# waypoint for a flight.
waypoint.waypoint_type = {
PointAction.TurningPoint: FlightWaypointType.NAV,
PointAction.FlyOverPoint: FlightWaypointType.NAV,
PointAction.FromParkingArea: FlightWaypointType.TAKEOFF,
PointAction.FromParkingAreaHot: FlightWaypointType.TAKEOFF,
PointAction.FromRunway: FlightWaypointType.TAKEOFF,
PointAction.FromGroundArea: FlightWaypointType.TAKEOFF,
PointAction.FromGroundAreaHot: FlightWaypointType.TAKEOFF,
}[point.action]
if waypoint.waypoint_type == FlightWaypointType.NAV:
waypoint.name = "NAV"
waypoint.pretty_name = "Nav"
waypoint.description = "Nav"
else:
waypoint.name = "TAKEOFF"
waypoint.pretty_name = "Takeoff"
waypoint.description = "Takeoff"
waypoint.description = f"Takeoff from {from_cp.name}"
return waypoint

View File

@ -1,4 +1,5 @@
import logging
from datetime import datetime
from typing import Any, Optional
from dcs.task import (

View File

@ -1,6 +1,7 @@
from __future__ import annotations
import logging
from datetime import datetime
from functools import cached_property
from typing import Any, Dict, List, TYPE_CHECKING
@ -11,6 +12,7 @@ from dcs.unitgroup import FlyingGroup, StaticGroup
from game.ato.airtaaskingorder import AirTaskingOrder
from game.ato.flight import Flight
from game.ato.flightstate import Completed
from game.ato.flighttype import FlightType
from game.ato.package import Package
from game.ato.starttype import StartType
@ -42,6 +44,7 @@ class AircraftGenerator:
mission: Mission,
settings: Settings,
game: Game,
time: datetime,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
laser_code_registry: LaserCodeRegistry,
@ -49,9 +52,10 @@ class AircraftGenerator:
air_support: AirSupport,
helipads: dict[ControlPoint, list[StaticGroup]],
) -> None:
self.m = mission
self.game = game
self.mission = mission
self.settings = settings
self.game = game
self.time = time
self.radio_registry = radio_registry
self.tacan_registy = tacan_registry
self.laser_code_registry = laser_code_registry
@ -144,9 +148,10 @@ class AircraftGenerator:
StartType.COLD,
divert=None,
)
flight.state = Completed(flight, self.game.settings)
group = FlightGroupSpawner(
flight, country, self.m, self.helipads
flight, country, self.mission, self.helipads
).create_idle_aircraft()
AircraftPainter(flight, group).apply_livery()
self.unit_map.add_aircraft(group, flight)
@ -154,15 +159,17 @@ class AircraftGenerator:
def create_and_configure_flight(
self, flight: Flight, country: Country, dynamic_runways: Dict[str, RunwayData]
) -> FlyingGroup[Any]:
"""Creates and configures the flight group in the mission."""
group = FlightGroupSpawner(
flight, country, self.m, self.helipads
flight, country, self.mission, self.helipads
).create_flight_group()
self.flights.append(
FlightGroupConfigurator(
flight,
group,
self.game,
self.m,
self.mission,
self.time,
self.radio_registry,
self.tacan_registy,
self.laser_code_registry,

View File

@ -1,6 +1,7 @@
from __future__ import annotations
import logging
from datetime import datetime
from typing import Any, Optional, TYPE_CHECKING
from dcs import Mission
@ -36,6 +37,7 @@ class FlightGroupConfigurator:
group: FlyingGroup[Any],
game: Game,
mission: Mission,
time: datetime,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
laser_code_registry: LaserCodeRegistry,
@ -47,6 +49,7 @@ class FlightGroupConfigurator:
self.group = group
self.game = game
self.mission = mission
self.time = time
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
self.laser_code_registry = laser_code_registry
@ -72,7 +75,13 @@ class FlightGroupConfigurator:
)
mission_start_time, waypoints = WaypointGenerator(
self.flight, self.group, self.mission, self.game.settings, self.air_support
self.flight,
self.group,
self.mission,
self.game.conditions.start_time,
self.time,
self.game.settings,
self.air_support,
).create_waypoints()
return FlightData(

View File

@ -12,6 +12,7 @@ from dcs.terrain import Airport, NoParkingSlotError
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
from game.ato import Flight
from game.ato.flightstate import InFlight
from game.ato.starttype import StartType
from game.theater import Airfield, ControlPoint, NavalControlPoint, OffMapSpawn
from game.utils import meters
@ -40,7 +41,31 @@ class FlightGroupSpawner:
self.helipads = helipads
def create_flight_group(self) -> FlyingGroup[Any]:
"""Creates the group for the flight and adds it to the mission.
Each flight is spawned according to its FlightState at the time of mission
generation. Aircraft that are WaitingForStart will be set up based on their
StartType with a delay. Note that delays are actually created during waypoint
generation.
Aircraft that are *not* WaitingForStart will be spawned in their current state.
We cannot spawn aircraft mid-taxi, so when the simulated state is near the end
of a long taxi period the aircraft will be spawned in their parking spot. This
could lead to problems but that's what loiter points are for. The other pre-
flight states have the same problem but are much shorter and more easily covered
by the loiter time. Player flights that are spawned near the end of their cold
start have the biggest problem but players are able to cut corners to make up
for lost time.
Aircraft that are already in the air will be spawned at their estimated
location, speed, and altitude based on their flight plan.
"""
if (
self.flight.state.is_waiting_for_start
or self.flight.state.spawn_type is not StartType.IN_FLIGHT
):
return self.generate_flight_at_departure()
return self.generate_mid_mission()
def create_idle_aircraft(self) -> FlyingGroup[Any]:
assert isinstance(self.flight.squadron.location, Airfield)
@ -54,13 +79,17 @@ class FlightGroupSpawner:
group.uncontrolled = True
return group
@property
def start_type(self) -> StartType:
return self.flight.state.spawn_type
def generate_flight_at_departure(self) -> FlyingGroup[Any]:
name = namegen.next_aircraft_name(
self.country, self.flight.departure.id, self.flight
)
cp = self.flight.departure
try:
if self.flight.start_type is StartType.IN_FLIGHT:
if self.start_type is StartType.IN_FLIGHT:
group = self._generate_over_departure(name, cp)
return group
elif isinstance(cp, NavalControlPoint):
@ -69,7 +98,7 @@ class FlightGroupSpawner:
if not isinstance(carrier_group, ShipGroup):
raise RuntimeError(
f"Carrier group {carrier_group} is a "
"{carrier_group.__class__.__name__}, expected a ShipGroup"
f"{carrier_group.__class__.__name__}, expected a ShipGroup"
)
return self._generate_at_group(name, carrier_group)
else:
@ -88,11 +117,33 @@ class FlightGroupSpawner:
logging.exception(
"No room on runway or parking slots. Starting from the air."
)
self.flight.start_type = StartType.IN_FLIGHT
group = self._generate_over_departure(name, cp)
group.points[0].alt = 1500
return group
def generate_mid_mission(self) -> FlyingGroup[Any]:
assert isinstance(self.flight.state, InFlight)
name = namegen.next_aircraft_name(
self.country, self.flight.departure.id, self.flight
)
speed = self.flight.state.estimate_speed()
pos = self.flight.state.estimate_position()
alt, alt_type = self.flight.state.estimate_altitude()
group = self.mission.flight_group(
country=self.country,
name=name,
aircraft_type=self.flight.unit_type.dcs_unit_type,
airport=None,
position=pos,
altitude=alt.meters,
speed=speed.kph,
maintask=None,
group_size=self.flight.count,
)
group.points[0].alt_type = alt_type
return group
def _generate_at_airport(self, name: str, airport: Airport) -> FlyingGroup[Any]:
# TODO: Delayed runway starts should be converted to air starts for multiplayer.
# Runway starts do not work with late activated aircraft in multiplayer. Instead
@ -169,7 +220,7 @@ class FlightGroupSpawner:
group.points[0].action = PointAction.FromGroundArea
group.points[0].type = "TakeOffGround"
group.units[0].heading = helipad.units[0].heading
if self.flight.start_type != "Cold":
if self.start_type is not StartType.COLD:
group.points[0].action = PointAction.FromGroundAreaHot
group.points[0].type = "TakeOffGroundHot"
@ -183,15 +234,13 @@ class FlightGroupSpawner:
return group
def dcs_start_type(self) -> DcsStartType:
if self.flight.start_type is StartType.RUNWAY:
if self.start_type is StartType.RUNWAY:
return DcsStartType.Runway
elif self.flight.start_type is StartType.COLD:
elif self.start_type is StartType.COLD:
return DcsStartType.Cold
elif self.flight.start_type is StartType.WARM:
elif self.start_type is StartType.WARM:
return DcsStartType.Warm
raise ValueError(
f"There is no pydcs StartType matching {self.flight.start_type}"
)
raise ValueError(f"There is no pydcs StartType matching {self.start_type}")
def _start_type_at_group(
self,

View File

@ -9,9 +9,7 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
class BaiIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
def add_tasks(self, waypoint: MovingPoint) -> None:
# TODO: Add common "UnitGroupTarget" base type.
group_names = []
target = self.package.target
@ -25,7 +23,7 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
"Unexpected target type for BAI mission: %s",
target.__class__.__name__,
)
return waypoint
return
for group_name in group_names:
group = self.mission.find_group(group_name)
@ -39,4 +37,3 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
task.params["altitudeEnabled"] = False
task.params["groupAttack"] = True
waypoint.tasks.append(task)
return waypoint

View File

@ -9,8 +9,7 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
class CasIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
def add_tasks(self, waypoint: MovingPoint) -> None:
if isinstance(self.flight.flight_plan, CasFlightPlan):
waypoint.add_task(
EngageTargetsInZone(
@ -35,4 +34,3 @@ class CasIngressBuilder(PydcsWaypointBuilder):
],
)
)
return waypoint

View File

@ -8,8 +8,7 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
class DeadIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
def add_tasks(self, waypoint: MovingPoint) -> None:
self.register_special_waypoints(self.waypoint.targets)
target = self.package.target
@ -18,7 +17,7 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
"Unexpected target type for DEAD mission: %s",
target.__class__.__name__,
)
return waypoint
return
for group in target.groups:
miz_group = self.mission.find_group(group.name)
@ -33,4 +32,3 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
task.params["altitudeEnabled"] = False
task.params["groupAttack"] = True
waypoint.tasks.append(task)
return waypoint

View File

@ -8,8 +8,7 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
class HoldPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
def add_tasks(self, waypoint: MovingPoint) -> None:
loiter = ControlledTask(
OrbitAction(altitude=waypoint.alt, pattern=OrbitAction.OrbitPattern.Circle)
)
@ -20,9 +19,10 @@ class HoldPointBuilder(PydcsWaypointBuilder):
f"{flight_plan_type} does not define a push time. AI will push "
"immediately and may flight unsuitable speeds."
)
return waypoint
return
push_time = self.flight.flight_plan.push_time
self.waypoint.departure_time = push_time
loiter.stop_after_time(int(push_time.total_seconds()))
loiter.stop_after_time(
int((push_time - self.elapsed_mission_time).total_seconds())
)
waypoint.add_task(loiter)
return waypoint

View File

@ -9,8 +9,7 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
class JoinPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
def add_tasks(self, waypoint: MovingPoint) -> None:
if self.flight.flight_type == FlightType.ESCORT:
self.configure_escort_tasks(
waypoint,
@ -23,7 +22,6 @@ class JoinPointBuilder(PydcsWaypointBuilder):
self.configure_escort_tasks(
waypoint, [Targets.All.GroundUnits.AirDefence.AAA.SAMRelated]
)
return waypoint
@staticmethod
def configure_escort_tasks(

View File

@ -9,16 +9,14 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
class OcaAircraftIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
def add_tasks(self, waypoint: MovingPoint) -> None:
target = self.package.target
if not isinstance(target, Airfield):
logging.error(
"Unexpected target type for OCA Strike mission: %s",
target.__class__.__name__,
)
return waypoint
return
task = EngageTargetsInZone(
position=target.position,
@ -32,4 +30,3 @@ class OcaAircraftIngressBuilder(PydcsWaypointBuilder):
task.params["altitudeEnabled"] = False
task.params["groupAttack"] = True
waypoint.tasks.append(task)
return waypoint

View File

@ -8,18 +8,15 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
class OcaRunwayIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
def add_tasks(self, waypoint: MovingPoint) -> None:
target = self.package.target
if not isinstance(target, Airfield):
logging.error(
"Unexpected target type for runway bombing mission: %s",
target.__class__.__name__,
)
return waypoint
return
waypoint.tasks.append(
BombingRunway(airport_id=target.airport.id, group_attack=True)
)
return waypoint

View File

@ -28,6 +28,7 @@ class PydcsWaypointBuilder:
group: FlyingGroup[Any],
flight: Flight,
mission: Mission,
elapsed_mission_time: timedelta,
air_support: AirSupport,
) -> None:
self.waypoint = waypoint
@ -35,6 +36,7 @@ class PydcsWaypointBuilder:
self.package = flight.package
self.flight = flight
self.mission = mission
self.elapsed_mission_time = elapsed_mission_time
self.air_support = air_support
def build(self) -> MovingPoint:
@ -54,12 +56,16 @@ class PydcsWaypointBuilder:
tot = self.flight.flight_plan.tot_for_waypoint(self.waypoint)
if tot is not None:
self.set_waypoint_tot(waypoint, tot)
self.add_tasks(waypoint)
return waypoint
def add_tasks(self, waypoint: MovingPoint) -> None:
pass
def set_waypoint_tot(self, waypoint: MovingPoint, tot: timedelta) -> None:
self.waypoint.tot = tot
if not self._viggen_client_tot():
waypoint.ETA = int(tot.total_seconds())
waypoint.ETA = int((tot - self.elapsed_mission_time).total_seconds())
waypoint.ETA_locked = True
waypoint.speed_locked = False

View File

@ -16,9 +16,7 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
class RaceTrackBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
def add_tasks(self, waypoint: MovingPoint) -> None:
flight_plan = self.flight.flight_plan
if not isinstance(flight_plan, PatrollingFlightPlan):
@ -27,7 +25,7 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
f"Cannot create race track for {self.flight} because "
f"{flight_plan_type} does not define a patrol."
)
return waypoint
return
# NB: It's important that the engage task comes before the orbit task.
# Though they're on the same waypoint, if the orbit task comes first it
@ -58,11 +56,10 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
racetrack = ControlledTask(orbit)
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)
racetrack.stop_after_time(int(flight_plan.patrol_end_time.total_seconds()))
loiter_duration = flight_plan.patrol_end_time - self.elapsed_mission_time
racetrack.stop_after_time(int(loiter_duration.total_seconds()))
waypoint.add_task(racetrack)
return waypoint
def configure_refueling_actions(self, waypoint: MovingPoint) -> None:
waypoint.add_task(Tanker())

View File

@ -8,8 +8,7 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
class SeadIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
def add_tasks(self, waypoint: MovingPoint) -> None:
self.register_special_waypoints(self.waypoint.targets)
target = self.package.target
@ -18,7 +17,7 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
"Unexpected target type for SEAD mission: %s",
target.__class__.__name__,
)
return waypoint
return
for group in target.groups:
miz_group = self.mission.find_group(group.name)
@ -33,4 +32,3 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
task.params["altitudeEnabled"] = False
task.params["groupAttack"] = True
waypoint.tasks.append(task)
return waypoint

View File

@ -7,18 +7,16 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
class StrikeIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
def add_tasks(self, waypoint: MovingPoint) -> None:
if self.group.units[0].unit_type in [B_17G, B_52H, Tu_22M3]:
return self.build_bombing()
self.add_bombing_tasks(waypoint)
else:
return self.build_strike()
def build_bombing(self) -> MovingPoint:
waypoint = super().build()
self.add_strike_tasks(waypoint)
def add_bombing_tasks(self, waypoint: MovingPoint) -> None:
targets = self.waypoint.targets
if not targets:
return waypoint
return
center = Point(0, 0)
for target in targets:
@ -33,10 +31,8 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
bombing.params["altitudeEnabled"] = False
bombing.params["groupAttack"] = True
waypoint.tasks.append(bombing)
return waypoint
def build_strike(self) -> MovingPoint:
waypoint = super().build()
def add_strike_tasks(self, waypoint: MovingPoint) -> None:
for target in self.waypoint.targets:
bombing = Bombing(target.position, weapon_type=WeaponType.Auto)
# If there is only one target, drop all ordnance in one pass.
@ -47,4 +43,3 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
# Register special waypoints
self.register_special_waypoints(self.waypoint.targets)
return waypoint

View File

@ -9,16 +9,14 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
class SweepIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
def add_tasks(self, waypoint: MovingPoint) -> None:
if not isinstance(self.flight.flight_plan, SweepFlightPlan):
flight_plan_type = self.flight.flight_plan.__class__.__name__
logging.error(
f"Cannot create sweep for {self.flight} because "
f"{flight_plan_type} is not a sweep flight plan."
)
return waypoint
return
waypoint.tasks.append(
EngageTargets(
@ -29,5 +27,3 @@ class SweepIngressBuilder(PydcsWaypointBuilder):
],
)
)
return waypoint

View File

@ -1,6 +1,6 @@
import itertools
import random
from datetime import timedelta
from datetime import datetime, timedelta
from typing import Any
from dcs import Mission
@ -12,15 +12,13 @@ from dcs.triggers import Event, TriggerOnce, TriggerRule
from dcs.unitgroup import FlyingGroup
from game.ato import Flight, FlightWaypoint
from game.ato.flightstate import InFlight, WaitingForStart
from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.starttype import StartType
from game.missiongenerator.airsupport import AirSupport
from game.settings import Settings
from game.theater import ControlPointType
from game.utils import meters, pairwise
from gen.flights.traveltime import TotEstimator
from .pydcswaypointbuilder import PydcsWaypointBuilder, TARGET_WAYPOINTS
from .baiingress import BaiIngressBuilder
from .cargostop import CargoStopBuilder
from .casingress import CasIngressBuilder
@ -31,6 +29,7 @@ from .joinpoint import JoinPointBuilder
from .landingpoint import LandingPointBuilder
from .ocaaircraftingress import OcaAircraftIngressBuilder
from .ocarunwayingress import OcaRunwayIngressBuilder
from .pydcswaypointbuilder import PydcsWaypointBuilder, TARGET_WAYPOINTS
from .racetrack import RaceTrackBuilder
from .racetrackend import RaceTrackEndBuilder
from .seadingress import SeadIngressBuilder
@ -44,12 +43,16 @@ class WaypointGenerator:
flight: Flight,
group: FlyingGroup[Any],
mission: Mission,
turn_start_time: datetime,
time: datetime,
settings: Settings,
air_support: AirSupport,
) -> None:
self.flight = flight
self.group = group
self.mission = mission
self.elapsed_mission_time = time - turn_start_time
self.time = time
self.settings = settings
self.air_support = air_support
@ -57,16 +60,21 @@ class WaypointGenerator:
for waypoint in self.flight.points:
waypoint.tot = None
takeoff_point = FlightWaypoint.from_pydcs(
self.group.points[0], self.flight.from_cp
)
mission_start_time = self.set_takeoff_time(takeoff_point)
waypoints = self.flight.flight_plan.waypoints
mission_start_time = self.set_takeoff_time(waypoints[0])
filtered_points: list[FlightWaypoint] = []
for point in self.flight.points:
if point.only_for_player and not self.flight.client_count:
continue
if isinstance(self.flight.state, InFlight):
if point == self.flight.state.current_waypoint:
# We don't need to build this waypoint because pydcs did that for
# us, but we do need to configure the tasks for it so that mid-
# mission aircraft starting at a waypoint with tasks behave
# correctly.
self.builder_for_waypoint(point).add_tasks(self.group.points[0])
if point not in self.flight.state.passed_waypoints:
filtered_points.append(point)
# Only add 1 target waypoint for Viggens. This only affects player flights, the
# Viggen can't have more than 9 waypoints which leaves us with two target point
@ -100,7 +108,6 @@ class WaypointGenerator:
# estimation ability the minimum fuel amounts will be calculated during flight
# plan construction, but for now it's only used by the kneeboard so is generated
# late.
waypoints = [takeoff_point] + self.flight.points
self._estimate_min_fuel_for(waypoints)
return mission_start_time, waypoints
@ -124,7 +131,12 @@ class WaypointGenerator:
}
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
return builder(
waypoint, self.group, self.flight, self.mission, self.air_support
waypoint,
self.group,
self.flight,
self.mission,
self.elapsed_mission_time,
self.air_support,
)
def _estimate_min_fuel_for(self, waypoints: list[FlightWaypoint]) -> None:
@ -171,23 +183,25 @@ class WaypointGenerator:
a.min_fuel = min_fuel
def set_takeoff_time(self, waypoint: FlightWaypoint) -> timedelta:
estimator = TotEstimator(self.flight.package)
start_time = estimator.mission_start_time(self.flight)
if isinstance(self.flight.state, WaitingForStart):
delay = self.flight.state.time_remaining(self.time)
else:
delay = timedelta()
if self.should_delay_flight(start_time):
if self.should_delay_flight():
if self.should_activate_late():
# Late activation causes the aircraft to not be spawned
# until triggered.
self.set_activation_time(start_time)
self.set_activation_time(delay)
elif self.flight.start_type is StartType.COLD:
# Setting the start time causes the AI to wait until the
# specified time to begin their startup sequence.
self.set_startup_time(start_time)
self.set_startup_time(delay)
# And setting *our* waypoint TOT causes the takeoff time to show up in
# the player's kneeboard.
waypoint.tot = self.flight.flight_plan.takeoff_time()
return start_time
return delay
def set_activation_time(self, delay: timedelta) -> None:
# Note: Late activation causes the waypoint TOTs to look *weird* in the
@ -232,17 +246,17 @@ class WaypointGenerator:
activation_trigger.add_action(AITaskPush(self.group.id, len(self.group.tasks)))
self.mission.triggerrules.triggers.append(activation_trigger)
def should_delay_flight(self, start_time: timedelta) -> bool:
if start_time.total_seconds() <= 0:
def should_delay_flight(self) -> bool:
if not isinstance(self.flight.state, WaitingForStart):
return False
if not self.flight.client_count:
return True
if start_time < timedelta(minutes=10):
# Don't bother delaying client flights with short start delays. Much
# more than ten minutes starts to eat into fuel a bit more
# (espeicially for something fuel limited like a Harrier).
if self.flight.state.time_remaining(self.time) < timedelta(minutes=10):
# Don't bother delaying client flights with short start delays. Much more
# than ten minutes starts to eat into fuel a bit more (especially for
# something fuel limited like a Harrier).
return False
return not self.settings.never_delay_player_flights

View File

@ -1,3 +1,4 @@
from datetime import datetime
from typing import Optional
from dcs.mission import Mission
@ -6,9 +7,12 @@ from game.weather import Clouds, Fog, Conditions, WindConditions, AtmosphericCon
class EnvironmentGenerator:
def __init__(self, mission: Mission, conditions: Conditions) -> None:
def __init__(
self, mission: Mission, conditions: Conditions, time: datetime
) -> None:
self.mission = mission
self.conditions = conditions
self.time = time
def set_atmospheric(self, atmospheric: AtmosphericConditions) -> None:
self.mission.weather.qnh = atmospheric.qnh.mm_hg
@ -35,7 +39,7 @@ class EnvironmentGenerator:
self.mission.weather.wind_at_8000 = wind.at_8000m
def generate(self) -> None:
self.mission.start_time = self.conditions.start_time
self.mission.start_time = self.time
self.set_atmospheric(self.conditions.weather.atmospheric)
self.set_clouds(self.conditions.weather.clouds)
self.set_fog(self.conditions.weather.fog)

View File

@ -1,6 +1,7 @@
from __future__ import annotations
import logging
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING, cast
@ -46,8 +47,9 @@ COMBINED_ARMS_SLOTS = 1
class MissionGenerator:
def __init__(self, game: Game) -> None:
def __init__(self, game: Game, time: datetime) -> None:
self.game = game
self.time = time
self.mission = Mission(game.theater.terrain)
self.unit_map = UnitMap()
@ -74,7 +76,7 @@ class MissionGenerator:
self.add_airfields_to_unit_map()
self.initialize_registries()
EnvironmentGenerator(self.mission, self.game.conditions).generate()
EnvironmentGenerator(self.mission, self.game.conditions, self.time).generate()
tgo_generator = TgoGenerator(
self.mission,
@ -232,6 +234,7 @@ class MissionGenerator:
self.mission,
self.game.settings,
self.game,
self.time,
self.radio_registry,
self.tacan_registry,
self.laser_code_registry,

View File

@ -286,6 +286,34 @@ class Settings:
# Mission Generator
# Gameplay
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."
),
)
player_mission_interrupts_sim_at: Optional[StartType] = choices_option(
"Player missions interrupt fast forward",
page=MISSION_GENERATOR_PAGE,
section=GAMEPLAY_SECTION,
default=None,
choices={
"Never": None,
"At startup time": StartType.COLD,
"At taxi time": StartType.WARM,
"At takeoff time": StartType.RUNWAY,
},
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."
),
)
supercarrier: bool = boolean_option(
"Use supercarrier module",
MISSION_GENERATOR_PAGE,

View File

@ -0,0 +1,94 @@
from __future__ import annotations
import itertools
import logging
from collections import Iterator
from datetime import datetime, timedelta
from typing_extensions import TYPE_CHECKING
from game.ato import Flight
from game.ato.flightstate import (
InFlight,
StartUp,
Takeoff,
Taxi,
Uninitialized,
WaitingForStart,
)
from game.ato.starttype import StartType
from gen.flights.traveltime import TotEstimator
if TYPE_CHECKING:
from game import Game
TICK = timedelta(seconds=1)
class AircraftSimulation:
def __init__(self, game: Game) -> None:
self.game = game
self.time = self.game.conditions.start_time
def run(self) -> None:
self.reset()
self.set_initial_flight_states()
if self.game.settings.fast_forward_to_first_contact:
self.simulate_until_first_contact()
logging.info(f"Mission simulation completed at {self.time}")
def simulate_until_first_contact(self) -> None:
while True:
self.time += TICK
if self.tick():
return
def tick(self) -> bool:
interrupt_sim = False
for flight in self.iter_flights():
if flight.on_game_tick(self.time, TICK):
interrupt_sim = True
# TODO: Check for SAM or A2A contact.
# Generate an engagement poly for all active air-to-air aircraft per-coalition
# and compare those against aircraft positions. If any aircraft intersects an
# enemy air-threat region, generate the mission. Also check against enemy SAM
# zones.
return interrupt_sim
def set_initial_flight_states(self) -> None:
now = self.game.conditions.start_time
for flight in self.iter_flights():
estimator = TotEstimator(flight.package)
start_time = estimator.mission_start_time(flight)
if start_time <= timedelta():
self.set_active_flight_state(flight, now)
else:
flight.set_state(
WaitingForStart(flight, self.game.settings, now + start_time)
)
def set_active_flight_state(self, flight: Flight, now: datetime) -> None:
if flight.start_type is StartType.COLD:
flight.set_state(StartUp(flight, self.game.settings, now))
elif flight.start_type is StartType.WARM:
flight.set_state(Taxi(flight, self.game.settings, now))
elif flight.start_type is StartType.RUNWAY:
flight.set_state(Takeoff(flight, self.game.settings, now))
elif flight.start_type is StartType.IN_FLIGHT:
flight.set_state(InFlight(flight, self.game.settings))
else:
raise ValueError(f"Unknown start type {flight.start_type} for {flight}")
def reset(self) -> None:
self.time = self.game.conditions.start_time
for flight in self.iter_flights():
flight.set_state(Uninitialized(flight, self.game.settings))
def iter_flights(self) -> Iterator[Flight]:
packages = itertools.chain(
self.game.blue.ato.packages, self.game.red.ato.packages
)
for package in packages:
yield from package.flights

View File

@ -6,6 +6,7 @@ from typing import Optional, TYPE_CHECKING
from game.debriefing import Debriefing
from game.missiongenerator import MissionGenerator
from game.sim.aircraftsimulation import AircraftSimulation
from game.sim.missionresultsprocessor import MissionResultsProcessor
from game.unitmap import UnitMap
@ -17,9 +18,15 @@ class MissionSimulation:
def __init__(self, game: Game) -> None:
self.game = game
self.unit_map: Optional[UnitMap] = None
self.time = game.conditions.start_time
def run(self) -> None:
sim = AircraftSimulation(self.game)
sim.run()
self.time = sim.time
def generate_miz(self, output: Path) -> None:
self.unit_map = MissionGenerator(self.game).generate_miz(output)
self.unit_map = MissionGenerator(self.game, self.time).generate_miz(output)
def debrief_current_state(
self, state_path: Path, force_end: bool = False

View File

@ -43,11 +43,11 @@ from .closestairfields import ObjectiveDistanceCache
from game.ato.flighttype import FlightType
from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.flightwaypoint import FlightWaypoint
from game.ato.flight import Flight
from .traveltime import GroundSpeed, TravelTime
from .waypointbuilder import StrikeTarget, WaypointBuilder
if TYPE_CHECKING:
from game.ato.flight import Flight
from game.ato.package import Package
from game.coalition import Coalition
from game.threatzones import ThreatZones

View File

@ -6,19 +6,17 @@ from datetime import timedelta
from typing import TYPE_CHECKING
from dcs.mapping import Point
from dcs.unittype import FlyingType
from game.utils import (
Distance,
SPEED_OF_SOUND_AT_SEA_LEVEL,
Speed,
kph,
mach,
meters,
)
from game.ato.flight import Flight
if TYPE_CHECKING:
from game.ato.flight import Flight
from game.ato.package import Package

View File

@ -18,10 +18,6 @@ from dcs.mapping import Point
from dcs.unit import Unit
from dcs.unitgroup import VehicleGroup, ShipGroup
if TYPE_CHECKING:
from game.coalition import Coalition
from game.transfers import MultiGroupTransport
from game.theater import (
ControlPoint,
MissionTarget,
@ -31,7 +27,11 @@ from game.theater import (
from game.utils import Distance, meters, nautical_miles
from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.flightwaypoint import FlightWaypoint
from game.ato.flight import Flight
if TYPE_CHECKING:
from game.ato.flight import Flight
from game.coalition import Coalition
from game.transfers import MultiGroupTransport
@dataclass(frozen=True)

View File

@ -278,6 +278,7 @@ class QTopPanel(QFrame):
return
sim = MissionSimulation(self.game)
sim.run()
sim.generate_miz(persistency.mission_path_for("liberation_nextturn.miz"))
waiting = QWaitingForMissionResultWindow(self.game, sim, self)