dcs-retribution/game/sim/combat/combatinitiator.py
Dan Albert 656a98675e Rework sim status update to not need a thread.
Rather than polling at 60Hz (which may be faster than the tick rate,
wasting cycles; and also makes synchronization annoying), collect events
during the tick and emit them after (rate limited, pooling events until
it is time for another event to send).

This can be improved by paying attention to the aircraft update list,
which would allow us to avoid updating aircraft that don't have a status
change. To do that we need to be able to quickly lookup a FlightJs
matching a Flight through, and Flight isn't hashable.

We should also be removing dead events and de-duplicating. Currently
each flight has an update for every tick, but only the latest one
matters. Combat update events also don't matter if the same combat is
new in the update.

https://github.com/dcs-liberation/dcs_liberation/issues/1680
2021-12-23 17:46:24 -08:00

116 lines
4.4 KiB
Python

from __future__ import annotations
import itertools
import logging
from collections.abc import Iterator
from typing import Optional, TYPE_CHECKING
from game.ato.flightstate import InFlight
from .aircombat import AirCombat
from .aircraftengagementzones import AircraftEngagementZones
from .atip import AtIp
from .defendingsam import DefendingSam
from .joinablecombat import JoinableCombat
from .samengagementzones import SamEngagementZones
from ..gameupdateevents import GameUpdateEvents
if TYPE_CHECKING:
from game import Game
from game.ato import Flight
from .frozencombat import FrozenCombat
class CombatInitiator:
def __init__(
self, game: Game, combats: list[FrozenCombat], events: GameUpdateEvents
) -> None:
self.game = game
self.combats = combats
self.events = events
def update_active_combats(self) -> None:
blue_a2a = AircraftEngagementZones.from_ato(self.game.blue.ato)
red_a2a = AircraftEngagementZones.from_ato(self.game.red.ato)
blue_sam = SamEngagementZones.from_theater(self.game.theater, player=True)
red_sam = SamEngagementZones.from_theater(self.game.theater, player=False)
# Check each vulnerable flight to see if it has initiated combat. If any flight
# initiates combat, a single FrozenCombat will be created for all involved
# flights and the FlightState of each flight will be updated accordingly.
#
# There's some nuance to this behavior. Duplicate combats are avoided because
# InCombat flight states are not considered vulnerable. That means that once an
# aircraft has entered combat it will not be rechecked later in the loop or on
# another tick.
for flight in self.iter_flights():
if flight.squadron.player:
a2a = red_a2a
own_a2a = blue_a2a
sam = red_sam
else:
a2a = blue_a2a
own_a2a = red_a2a
sam = blue_sam
self.check_flight_for_combat(flight, a2a, own_a2a, sam)
def check_flight_for_combat(
self,
flight: Flight,
a2a: AircraftEngagementZones,
own_a2a: AircraftEngagementZones,
sam: SamEngagementZones,
) -> None:
if (joined := self.check_flight_for_joined_combat(flight)) is not None:
logging.info(f"{flight} is joining existing combat {joined}")
joined.join(flight)
own_a2a.remove_flight(flight)
self.events.update_combat(joined)
elif (combat := self.check_flight_for_new_combat(flight, a2a, sam)) is not None:
logging.info(f"Interrupting simulation because {combat.because()}")
combat.update_flight_states()
# Remove any preoccupied flights from the list of potential air-to-air
# threats. This prevents BARCAPs (and other air-to-air types) from getting
# involved in multiple combats simultaneously. Additional air-to-air
# aircraft may join existing combats, but they will not create new combats.
a2a.update_for_combat(combat)
own_a2a.update_for_combat(combat)
self.combats.append(combat)
self.events.new_combat(combat)
def check_flight_for_joined_combat(
self, flight: Flight
) -> Optional[JoinableCombat]:
for combat in self.combats:
if isinstance(combat, JoinableCombat) and combat.joinable_by(flight):
return combat
return None
@staticmethod
def check_flight_for_new_combat(
flight: Flight, a2a: AircraftEngagementZones, sam: SamEngagementZones
) -> Optional[FrozenCombat]:
if not isinstance(flight.state, InFlight):
return None
if flight.state.is_at_ip:
return AtIp(flight)
position = flight.state.estimate_position()
if flight.state.vulnerable_to_intercept and a2a.covers(position):
flights = [flight]
flights.extend(a2a.iter_intercepting_flights(position))
return AirCombat(flights)
if flight.state.vulnerable_to_sam and sam.covers(position):
return DefendingSam(flight, list(sam.iter_threatening_sams(position)))
return None
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