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:
parent
43d5dc0528
commit
656a98675e
@ -11,6 +11,7 @@ from .flightstate import FlightState, Uninitialized
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
from game.squadrons import Squadron, Pilot
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
from game.transfers import TransferOrder
|
||||
@ -147,8 +148,10 @@ class Flight:
|
||||
def set_state(self, state: FlightState) -> None:
|
||||
self.state = state
|
||||
|
||||
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
|
||||
self.state.on_game_tick(time, duration)
|
||||
def on_game_tick(
|
||||
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
||||
) -> None:
|
||||
self.state.on_game_tick(events, time, duration)
|
||||
|
||||
def should_halt_sim(self) -> bool:
|
||||
return self.state.should_halt_sim()
|
||||
|
||||
@ -1,11 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .flightstate import FlightState
|
||||
from ..starttype import StartType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
|
||||
|
||||
class Completed(FlightState):
|
||||
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
|
||||
def on_game_tick(
|
||||
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
||||
) -> None:
|
||||
return
|
||||
|
||||
@property
|
||||
|
||||
@ -9,6 +9,7 @@ from game.ato.starttype import StartType
|
||||
if TYPE_CHECKING:
|
||||
from game.ato.flight import Flight
|
||||
from game.settings import Settings
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
from game.threatzones import ThreatPoly
|
||||
|
||||
|
||||
@ -18,7 +19,9 @@ class FlightState(ABC):
|
||||
self.settings = settings
|
||||
|
||||
@abstractmethod
|
||||
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
|
||||
def on_game_tick(
|
||||
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@property
|
||||
|
||||
@ -11,6 +11,7 @@ from ..starttype import StartType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.sim.combat import FrozenCombat
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
|
||||
|
||||
class InCombat(InFlight):
|
||||
@ -32,7 +33,9 @@ class InCombat(InFlight):
|
||||
def estimate_speed(self) -> Speed:
|
||||
return self.previous_state.estimate_speed()
|
||||
|
||||
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
|
||||
def on_game_tick(
|
||||
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
||||
) -> None:
|
||||
raise RuntimeError("Cannot simulate combat")
|
||||
|
||||
@property
|
||||
|
||||
@ -17,6 +17,7 @@ from gen.flights.flightplan import LoiterFlightPlan
|
||||
if TYPE_CHECKING:
|
||||
from game.ato.flight import Flight
|
||||
from game.settings import Settings
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
|
||||
|
||||
class InFlight(FlightState, ABC):
|
||||
@ -88,7 +89,9 @@ class InFlight(FlightState, ABC):
|
||||
def advance_to_next_waypoint(self) -> None:
|
||||
self.flight.set_state(self.next_waypoint_state())
|
||||
|
||||
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
|
||||
def on_game_tick(
|
||||
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
||||
) -> None:
|
||||
self.elapsed_time += duration
|
||||
if self.elapsed_time > self.total_time_to_next_waypoint:
|
||||
self.advance_to_next_waypoint()
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dcs import Point
|
||||
@ -9,7 +10,7 @@ from game.ato.starttype import StartType
|
||||
from game.utils import Distance, LBS_TO_KG, Speed, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
|
||||
|
||||
def lerp(v0: float, v1: float, t: float) -> float:
|
||||
@ -17,6 +18,12 @@ def lerp(v0: float, v1: float, t: float) -> float:
|
||||
|
||||
|
||||
class Navigating(InFlight):
|
||||
def on_game_tick(
|
||||
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
||||
) -> None:
|
||||
super().on_game_tick(events, time, duration)
|
||||
events.update_flight(self.flight)
|
||||
|
||||
def progress(self) -> float:
|
||||
return (
|
||||
self.elapsed_time.total_seconds()
|
||||
|
||||
@ -11,6 +11,7 @@ from ..starttype import StartType
|
||||
if TYPE_CHECKING:
|
||||
from game.ato.flight import Flight
|
||||
from game.settings import Settings
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
|
||||
|
||||
class StartUp(FlightState):
|
||||
@ -18,7 +19,9 @@ class StartUp(FlightState):
|
||||
super().__init__(flight, settings)
|
||||
self.completion_time = now + flight.flight_plan.estimate_startup()
|
||||
|
||||
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
|
||||
def on_game_tick(
|
||||
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
||||
) -> None:
|
||||
if time < self.completion_time:
|
||||
return
|
||||
self.flight.set_state(Taxi(self.flight, self.settings, time))
|
||||
|
||||
@ -12,6 +12,7 @@ from ...utils import LBS_TO_KG
|
||||
if TYPE_CHECKING:
|
||||
from game.ato.flight import Flight
|
||||
from game.settings import Settings
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
|
||||
|
||||
class Takeoff(FlightState):
|
||||
@ -20,7 +21,9 @@ class Takeoff(FlightState):
|
||||
# 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:
|
||||
def on_game_tick(
|
||||
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
||||
) -> None:
|
||||
if time < self.completion_time:
|
||||
return
|
||||
self.flight.set_state(Navigating(self.flight, self.settings, waypoint_index=0))
|
||||
|
||||
@ -11,6 +11,7 @@ from ..starttype import StartType
|
||||
if TYPE_CHECKING:
|
||||
from game.ato.flight import Flight
|
||||
from game.settings import Settings
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
|
||||
|
||||
class Taxi(FlightState):
|
||||
@ -18,7 +19,9 @@ class Taxi(FlightState):
|
||||
super().__init__(flight, settings)
|
||||
self.completion_time = now + flight.flight_plan.estimate_ground_ops()
|
||||
|
||||
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
|
||||
def on_game_tick(
|
||||
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
||||
) -> None:
|
||||
if time < self.completion_time:
|
||||
return
|
||||
self.flight.set_state(Takeoff(self.flight, self.settings, time))
|
||||
|
||||
@ -1,12 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from .flightstate import FlightState
|
||||
from ..starttype import StartType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
|
||||
|
||||
class Uninitialized(FlightState):
|
||||
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
|
||||
def on_game_tick(
|
||||
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
||||
) -> None:
|
||||
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
|
||||
|
||||
@property
|
||||
|
||||
@ -13,6 +13,7 @@ from .taxi import Taxi
|
||||
if TYPE_CHECKING:
|
||||
from game.ato.flight import Flight
|
||||
from game.settings import Settings
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
|
||||
|
||||
class WaitingForStart(FlightState):
|
||||
@ -29,7 +30,9 @@ class WaitingForStart(FlightState):
|
||||
def start_type(self) -> StartType:
|
||||
return self.flight.start_type
|
||||
|
||||
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
|
||||
def on_game_tick(
|
||||
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
||||
) -> None:
|
||||
if time < self.start_time:
|
||||
return
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -17,6 +17,7 @@ from game.ato.flight import Flight
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.ato.package import Package
|
||||
from game.game import Game
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
from game.squadrons.squadron import Pilot, Squadron
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from game.transfers import PendingTransfers, TransferOrder
|
||||
@ -211,7 +212,7 @@ class PackageModel(QAbstractListModel):
|
||||
for flight in self.package.flights:
|
||||
yield flight
|
||||
|
||||
def on_sim_update(self) -> None:
|
||||
def on_sim_update(self, _events: GameUpdateEvents) -> None:
|
||||
self.dataChanged.emit(self.index(0), self.index(self.rowCount()))
|
||||
|
||||
|
||||
@ -311,7 +312,7 @@ class AtoModel(QAbstractListModel):
|
||||
for package in self.ato.packages:
|
||||
yield self.package_models.acquire(package)
|
||||
|
||||
def on_sim_update(self) -> None:
|
||||
def on_sim_update(self, _events: GameUpdateEvents) -> None:
|
||||
self.dataChanged.emit(self.index(0), self.index(self.rowCount()))
|
||||
|
||||
|
||||
|
||||
@ -8,11 +8,10 @@ from typing import Callable, Optional, TYPE_CHECKING
|
||||
from PySide2.QtCore import QObject, Signal
|
||||
|
||||
from game.polldebriefingfilethread import PollDebriefingFileThread
|
||||
from game.sim.combat import FrozenCombat
|
||||
from game.sim.gameloop import GameLoop
|
||||
from game.sim.gameupdatecallbacks import GameUpdateCallbacks
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
from game.sim.simspeedsetting import SimSpeedSetting
|
||||
from qt_ui.simupdatethread import SimUpdateThread
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@ -20,19 +19,15 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class SimController(QObject):
|
||||
sim_update = Signal()
|
||||
sim_update = Signal(GameUpdateEvents)
|
||||
sim_speed_reset = Signal(SimSpeedSetting)
|
||||
simulation_complete = Signal()
|
||||
on_add_combat = Signal(FrozenCombat)
|
||||
on_combat_changed = Signal(FrozenCombat)
|
||||
|
||||
def __init__(self, game: Optional[Game]) -> None:
|
||||
super().__init__()
|
||||
self.game_loop: Optional[GameLoop] = None
|
||||
self.recreate_game_loop(game)
|
||||
self.started = False
|
||||
self._sim_update_thread = SimUpdateThread(self.sim_update.emit)
|
||||
self._sim_update_thread.start()
|
||||
|
||||
@property
|
||||
def completed(self) -> bool:
|
||||
@ -54,12 +49,8 @@ class SimController(QObject):
|
||||
self.recreate_game_loop(game)
|
||||
self.sim_speed_reset.emit(SimSpeedSetting.PAUSED)
|
||||
|
||||
def shut_down(self) -> None:
|
||||
self._sim_update_thread.stop()
|
||||
|
||||
def recreate_game_loop(self, game: Optional[Game]) -> None:
|
||||
if self.game_loop is not None:
|
||||
self._sim_update_thread.on_sim_pause()
|
||||
self.game_loop.pause()
|
||||
self.game_loop = None
|
||||
if game is not None:
|
||||
@ -67,8 +58,7 @@ class SimController(QObject):
|
||||
game,
|
||||
GameUpdateCallbacks(
|
||||
self.on_simulation_complete,
|
||||
self.on_add_combat.emit,
|
||||
self.on_combat_changed.emit,
|
||||
self.sim_update.emit,
|
||||
),
|
||||
)
|
||||
self.started = False
|
||||
@ -81,17 +71,11 @@ class SimController(QObject):
|
||||
self.game_loop.start()
|
||||
self.started = True
|
||||
self.game_loop.set_simulation_speed(simulation_speed)
|
||||
if simulation_speed is SimSpeedSetting.PAUSED:
|
||||
self._sim_update_thread.on_sim_pause()
|
||||
else:
|
||||
self._sim_update_thread.on_sim_unpause()
|
||||
|
||||
def run_to_first_contact(self) -> None:
|
||||
self.game_loop.run_to_first_contact()
|
||||
self.sim_update.emit()
|
||||
|
||||
def generate_miz(self, output: Path) -> None:
|
||||
self._sim_update_thread.on_sim_pause()
|
||||
self.game_loop.pause_and_generate_miz(output)
|
||||
|
||||
def wait_for_debriefing(
|
||||
@ -104,14 +88,11 @@ class SimController(QObject):
|
||||
def debrief_current_state(
|
||||
self, state_path: Path, force_end: bool = False
|
||||
) -> Debriefing:
|
||||
self._sim_update_thread.on_sim_pause()
|
||||
return self.game_loop.pause_and_debrief(state_path, force_end)
|
||||
|
||||
def process_results(self, debriefing: Debriefing) -> None:
|
||||
self._sim_update_thread.on_sim_pause()
|
||||
return self.game_loop.complete_with_results(debriefing)
|
||||
|
||||
def on_simulation_complete(self) -> None:
|
||||
logging.debug("Simulation complete")
|
||||
self._sim_update_thread.on_sim_pause()
|
||||
self.simulation_complete.emit()
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
from threading import Event, Thread, Timer
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class SimUpdateThread(Thread):
|
||||
def __init__(self, update_callback: Callable[[], None]) -> None:
|
||||
super().__init__()
|
||||
self.update_callback = update_callback
|
||||
self.running = False
|
||||
self.should_shutdown = False
|
||||
self._interrupt = Event()
|
||||
self._timer = self._make_timer()
|
||||
|
||||
def run(self) -> None:
|
||||
while True:
|
||||
self._interrupt.wait()
|
||||
self._interrupt.clear()
|
||||
if self.should_shutdown:
|
||||
return
|
||||
if self.running:
|
||||
self.update_callback()
|
||||
self._timer = self._make_timer()
|
||||
self._timer.start()
|
||||
|
||||
def on_sim_pause(self) -> None:
|
||||
self._timer.cancel()
|
||||
self._timer = self._make_timer()
|
||||
self.running = False
|
||||
|
||||
def on_sim_unpause(self) -> None:
|
||||
if not self.running:
|
||||
self.running = True
|
||||
self._timer.start()
|
||||
|
||||
def stop(self) -> None:
|
||||
self.should_shutdown = True
|
||||
self._interrupt.set()
|
||||
|
||||
def on_timer_elapsed(self) -> None:
|
||||
self._timer = self._make_timer()
|
||||
self._timer.start()
|
||||
self._interrupt.set()
|
||||
|
||||
def _make_timer(self) -> Timer:
|
||||
return Timer(1 / 60, lambda: self._interrupt.set())
|
||||
@ -12,6 +12,7 @@ from PySide2.QtWidgets import (
|
||||
from dcs.weather import CloudPreset, Weather as PydcsWeather
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
from game.utils import mps
|
||||
from game.weather import Conditions, TimeOfDay
|
||||
from qt_ui.simcontroller import SimController
|
||||
@ -55,7 +56,7 @@ class QTimeTurnWidget(QGroupBox):
|
||||
|
||||
sim_controller.sim_update.connect(self.on_sim_update)
|
||||
|
||||
def on_sim_update(self) -> None:
|
||||
def on_sim_update(self, _events: GameUpdateEvents) -> None:
|
||||
time = self.sim_controller.current_time_in_sim
|
||||
if time is None:
|
||||
self.date_display.setText("")
|
||||
|
||||
@ -13,6 +13,7 @@ from game.sim.combat import FrozenCombat
|
||||
from game.sim.combat.aircombat import AirCombat
|
||||
from game.sim.combat.atip import AtIp
|
||||
from game.sim.combat.defendingsam import DefendingSam
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
from game.theater import (
|
||||
ConflictTheater,
|
||||
)
|
||||
@ -106,8 +107,6 @@ class MapModel(QObject):
|
||||
self.set_flight_selection
|
||||
)
|
||||
sim_controller.sim_update.connect(self.on_sim_update)
|
||||
sim_controller.on_add_combat.connect(self.on_add_combat)
|
||||
sim_controller.on_combat_changed.connect(self.on_combat_changed)
|
||||
self.reset()
|
||||
|
||||
def clear(self) -> None:
|
||||
@ -128,9 +127,17 @@ class MapModel(QObject):
|
||||
self._ip_combats = []
|
||||
self.cleared.emit()
|
||||
|
||||
def on_sim_update(self) -> None:
|
||||
def on_sim_update(self, events: GameUpdateEvents) -> None:
|
||||
# TODO: Only update flights with changes.
|
||||
# We have the signal of which flights have updates, but no fast lookup for
|
||||
# Flight -> FlightJs since Flight isn't hashable. Faster to update every flight
|
||||
# than do do the O(n^2) filtered update.
|
||||
for flight in self._flights.values():
|
||||
flight.positionChanged.emit()
|
||||
for combat in events.new_combats:
|
||||
self.on_add_combat(combat)
|
||||
for combat in events.updated_combats:
|
||||
self.on_combat_changed(combat)
|
||||
|
||||
def set_package_selection(self, index: int) -> None:
|
||||
self.deselect_current_flight()
|
||||
|
||||
@ -404,7 +404,6 @@ class QLiberationWindow(QMainWindow):
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
)
|
||||
if result == QMessageBox.Yes:
|
||||
self.sim_controller.shut_down()
|
||||
super().closeEvent(event)
|
||||
self.dialog = None
|
||||
else:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user