mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
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
This commit is contained in:
@@ -18,35 +18,36 @@ from game.ato.flightstate import (
|
||||
from game.ato.starttype import StartType
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from .combat import CombatInitiator, FrozenCombat
|
||||
from .gameupdatecallbacks import GameUpdateCallbacks
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from .gameupdateevents import GameUpdateEvents
|
||||
|
||||
|
||||
class AircraftSimulation:
|
||||
def __init__(self, game: Game, callbacks: GameUpdateCallbacks) -> None:
|
||||
def __init__(self, game: Game) -> None:
|
||||
self.game = game
|
||||
self.callbacks = callbacks
|
||||
self.combats: list[FrozenCombat] = []
|
||||
|
||||
def begin_simulation(self) -> None:
|
||||
self.reset()
|
||||
self.set_initial_flight_states()
|
||||
|
||||
def on_game_tick(self, time: datetime, duration: timedelta) -> bool:
|
||||
def on_game_tick(
|
||||
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
||||
) -> None:
|
||||
for flight in self.iter_flights():
|
||||
flight.on_game_tick(time, duration)
|
||||
flight.on_game_tick(events, time, duration)
|
||||
|
||||
# Finish updating all flights before checking for combat so that the new
|
||||
# positions are used.
|
||||
CombatInitiator(self.game, self.combats, self.callbacks).update_active_combats()
|
||||
CombatInitiator(self.game, self.combats, events).update_active_combats()
|
||||
|
||||
# After updating all combat states, check for halts.
|
||||
for flight in self.iter_flights():
|
||||
if flight.should_halt_sim():
|
||||
return True
|
||||
return False
|
||||
events.complete_simulation()
|
||||
return
|
||||
|
||||
def set_initial_flight_states(self) -> None:
|
||||
now = self.game.conditions.start_time
|
||||
|
||||
@@ -12,7 +12,7 @@ from .atip import AtIp
|
||||
from .defendingsam import DefendingSam
|
||||
from .joinablecombat import JoinableCombat
|
||||
from .samengagementzones import SamEngagementZones
|
||||
from ..gameupdatecallbacks import GameUpdateCallbacks
|
||||
from ..gameupdateevents import GameUpdateEvents
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@@ -22,11 +22,11 @@ if TYPE_CHECKING:
|
||||
|
||||
class CombatInitiator:
|
||||
def __init__(
|
||||
self, game: Game, combats: list[FrozenCombat], callbacks: GameUpdateCallbacks
|
||||
self, game: Game, combats: list[FrozenCombat], events: GameUpdateEvents
|
||||
) -> None:
|
||||
self.game = game
|
||||
self.combats = combats
|
||||
self.callbacks = callbacks
|
||||
self.events = events
|
||||
|
||||
def update_active_combats(self) -> None:
|
||||
blue_a2a = AircraftEngagementZones.from_ato(self.game.blue.ato)
|
||||
@@ -64,7 +64,7 @@ class CombatInitiator:
|
||||
logging.info(f"{flight} is joining existing combat {joined}")
|
||||
joined.join(flight)
|
||||
own_a2a.remove_flight(flight)
|
||||
self.callbacks.on_combat_changed(joined)
|
||||
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()
|
||||
@@ -75,7 +75,7 @@ class CombatInitiator:
|
||||
a2a.update_for_combat(combat)
|
||||
own_a2a.update_for_combat(combat)
|
||||
self.combats.append(combat)
|
||||
self.callbacks.on_add_combat(combat)
|
||||
self.events.new_combat(combat)
|
||||
|
||||
def check_flight_for_joined_combat(
|
||||
self, flight: Flight
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from .gamelooptimer import GameLoopTimer
|
||||
from .gameupdatecallbacks import GameUpdateCallbacks
|
||||
from .gameupdateevents import GameUpdateEvents
|
||||
from .missionsimulation import MissionSimulation, SimulationAlreadyCompletedError
|
||||
from .simspeedsetting import SimSpeedSetting
|
||||
|
||||
@@ -20,7 +21,9 @@ class GameLoop:
|
||||
self.game = game
|
||||
self.callbacks = callbacks
|
||||
self.timer = GameLoopTimer(self.tick)
|
||||
self.sim = MissionSimulation(self.game, self.callbacks)
|
||||
self.sim = MissionSimulation(self.game)
|
||||
self.events = GameUpdateEvents()
|
||||
self.last_update_time = datetime.now()
|
||||
self.started = False
|
||||
self.completed = False
|
||||
|
||||
@@ -53,7 +56,7 @@ class GameLoop:
|
||||
self.pause()
|
||||
logging.info("Running sim to first contact")
|
||||
while not self.completed:
|
||||
self.tick()
|
||||
self.tick(suppress_events=True)
|
||||
|
||||
def pause_and_generate_miz(self, output: Path) -> None:
|
||||
self.pause()
|
||||
@@ -68,13 +71,25 @@ class GameLoop:
|
||||
self.sim.process_results(debriefing)
|
||||
self.completed = True
|
||||
|
||||
def tick(self) -> None:
|
||||
def send_update(self, rate_limit: bool) -> None:
|
||||
now = datetime.now()
|
||||
time_since_update = now - self.last_update_time
|
||||
if not rate_limit or time_since_update >= timedelta(seconds=1 / 60):
|
||||
self.callbacks.on_update(self.events)
|
||||
self.events = GameUpdateEvents()
|
||||
self.last_update_time = now
|
||||
|
||||
def tick(self, suppress_events: bool = False) -> None:
|
||||
if not self.started:
|
||||
raise RuntimeError("Attempted to tick game loop before initialization")
|
||||
try:
|
||||
self.completed = self.sim.tick()
|
||||
self.sim.tick(self.events)
|
||||
self.completed = self.events.simulation_complete
|
||||
if not suppress_events:
|
||||
self.send_update(rate_limit=True)
|
||||
if self.completed:
|
||||
self.pause()
|
||||
self.send_update(rate_limit=False)
|
||||
logging.info(f"Simulation completed at {self.sim.time}")
|
||||
self.callbacks.on_simulation_complete()
|
||||
except SimulationAlreadyCompletedError:
|
||||
|
||||
@@ -2,10 +2,8 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.sim.combat import FrozenCombat
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
|
||||
|
||||
# Ought to be frozen but mypy can't handle that:
|
||||
@@ -13,5 +11,4 @@ if TYPE_CHECKING:
|
||||
@dataclass
|
||||
class GameUpdateCallbacks:
|
||||
on_simulation_complete: Callable[[], None]
|
||||
on_add_combat: Callable[[FrozenCombat], None]
|
||||
on_combat_changed: Callable[[FrozenCombat], None]
|
||||
on_update: Callable[[GameUpdateEvents], None]
|
||||
|
||||
27
game/sim/gameupdateevents.py
Normal file
27
game/sim/gameupdateevents.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato import Flight
|
||||
from game.sim.combat import FrozenCombat
|
||||
|
||||
|
||||
class GameUpdateEvents:
|
||||
def __init__(self) -> None:
|
||||
self.simulation_complete = False
|
||||
self.new_combats: list[FrozenCombat] = []
|
||||
self.updated_combats: list[FrozenCombat] = []
|
||||
self.updated_flights: list[Flight] = []
|
||||
|
||||
def complete_simulation(self) -> None:
|
||||
self.simulation_complete = True
|
||||
|
||||
def new_combat(self, combat: FrozenCombat) -> None:
|
||||
self.new_combats.append(combat)
|
||||
|
||||
def update_combat(self, combat: FrozenCombat) -> None:
|
||||
self.updated_combats.append(combat)
|
||||
|
||||
def update_flight(self, flight: Flight) -> None:
|
||||
self.updated_flights.append(flight)
|
||||
@@ -9,11 +9,11 @@ from game.debriefing import Debriefing
|
||||
from game.missiongenerator import MissionGenerator
|
||||
from game.unitmap import UnitMap
|
||||
from .aircraftsimulation import AircraftSimulation
|
||||
from .gameupdatecallbacks import GameUpdateCallbacks
|
||||
from .missionresultsprocessor import MissionResultsProcessor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from .gameupdateevents import GameUpdateEvents
|
||||
|
||||
|
||||
TICK = timedelta(seconds=1)
|
||||
@@ -25,10 +25,10 @@ class SimulationAlreadyCompletedError(RuntimeError):
|
||||
|
||||
|
||||
class MissionSimulation:
|
||||
def __init__(self, game: Game, callbacks: GameUpdateCallbacks) -> None:
|
||||
def __init__(self, game: Game) -> None:
|
||||
self.game = game
|
||||
self.unit_map: Optional[UnitMap] = None
|
||||
self.aircraft_simulation = AircraftSimulation(self.game, callbacks)
|
||||
self.aircraft_simulation = AircraftSimulation(self.game)
|
||||
self.completed = False
|
||||
self.time = self.game.conditions.start_time
|
||||
|
||||
@@ -36,12 +36,13 @@ class MissionSimulation:
|
||||
self.time = self.game.conditions.start_time
|
||||
self.aircraft_simulation.begin_simulation()
|
||||
|
||||
def tick(self) -> bool:
|
||||
def tick(self, events: GameUpdateEvents) -> GameUpdateEvents:
|
||||
self.time += TICK
|
||||
if self.completed:
|
||||
raise RuntimeError("Simulation already completed")
|
||||
self.completed = self.aircraft_simulation.on_game_tick(self.time, TICK)
|
||||
return self.completed
|
||||
self.aircraft_simulation.on_game_tick(events, self.time, TICK)
|
||||
self.completed = events.simulation_complete
|
||||
return events
|
||||
|
||||
def generate_miz(self, output: Path) -> None:
|
||||
self.unit_map = MissionGenerator(self.game, self.time).generate_miz(output)
|
||||
|
||||
Reference in New Issue
Block a user