diff --git a/game/ato/package.py b/game/ato/package.py index 43788cd5..1362a19e 100644 --- a/game/ato/package.py +++ b/game/ato/package.py @@ -4,10 +4,11 @@ import logging from collections import defaultdict from dataclasses import dataclass, field from datetime import timedelta -from typing import List, Optional, Dict, TYPE_CHECKING +from typing import Dict, List, Optional, TYPE_CHECKING from game.ato import Flight, FlightType from game.ato.packagewaypoints import PackageWaypoints +from game.db import Database from game.utils import Speed from gen.flights.flightplan import FormationFlightPlan from gen.flights.traveltime import TotEstimator @@ -24,6 +25,8 @@ class Package: #: TheaterGroundObject (non-ControlPoint map objectives). target: MissionTarget + _db: Database[Flight] + #: The set of flights in the package. flights: List[Flight] = field(default_factory=list) @@ -119,10 +122,12 @@ class Package: def add_flight(self, flight: Flight) -> None: """Adds a flight to the package.""" self.flights.append(flight) + self._db.add(flight.id, flight) def remove_flight(self, flight: Flight) -> None: """Removes a flight from the package.""" self.flights.remove(flight) + self._db.remove(flight.id) if not self.flights: self.waypoints = None diff --git a/game/commander/packagebuilder.py b/game/commander/packagebuilder.py index a1f5d195..79e86e7a 100644 --- a/game/commander/packagebuilder.py +++ b/game/commander/packagebuilder.py @@ -2,11 +2,12 @@ from __future__ import annotations from typing import Optional, TYPE_CHECKING +from game.theater import ControlPoint, MissionTarget, OffMapSpawn from game.utils import nautical_miles -from ..ato.package import Package -from game.theater import MissionTarget, OffMapSpawn, ControlPoint from ..ato.flight import Flight +from ..ato.package import Package from ..ato.starttype import StartType +from ..db.database import Database if TYPE_CHECKING: from game.dcs.aircrafttype import AircraftType @@ -23,6 +24,7 @@ class PackageBuilder: location: MissionTarget, closest_airfields: ClosestAirfields, air_wing: AirWing, + flight_db: Database[Flight], is_player: bool, package_country: str, start_type: StartType, @@ -31,7 +33,7 @@ class PackageBuilder: self.closest_airfields = closest_airfields self.is_player = is_player self.package_country = package_country - self.package = Package(location, auto_asap=asap) + self.package = Package(location, flight_db, auto_asap=asap) self.air_wing = air_wing self.start_type = start_type diff --git a/game/commander/packagefulfiller.py b/game/commander/packagefulfiller.py index b9262d3d..398d9476 100644 --- a/game/commander/packagefulfiller.py +++ b/game/commander/packagefulfiller.py @@ -2,24 +2,26 @@ from __future__ import annotations import logging from collections import defaultdict -from typing import Set, Iterable, Dict, TYPE_CHECKING, Optional +from typing import Dict, Iterable, Optional, Set, TYPE_CHECKING -from game.commander.missionproposals import ProposedMission, ProposedFlight, EscortType +from game.ato.airtaaskingorder import AirTaskingOrder +from game.ato.flighttype import FlightType +from game.ato.package import Package +from game.commander.missionproposals import EscortType, ProposedFlight, ProposedMission from game.commander.packagebuilder import PackageBuilder from game.data.doctrine import Doctrine +from game.db import Database from game.procurement import AircraftProcurementRequest from game.profiling import MultiEventTracer from game.settings import Settings from game.squadrons import AirWing from game.theater import ConflictTheater from game.threatzones import ThreatZones -from game.ato.airtaaskingorder import AirTaskingOrder -from game.ato.package import Package from gen.flights.closestairfields import ObjectiveDistanceCache -from game.ato.flighttype import FlightType from gen.flights.flightplan import FlightPlanBuilder if TYPE_CHECKING: + from game.ato import Flight from game.coalition import Coalition @@ -27,10 +29,15 @@ class PackageFulfiller: """Responsible for package aircraft allocation and flight plan layout.""" def __init__( - self, coalition: Coalition, theater: ConflictTheater, settings: Settings + self, + coalition: Coalition, + theater: ConflictTheater, + flight_db: Database[Flight], + settings: Settings, ) -> None: self.coalition = coalition self.theater = theater + self.flight_db = flight_db self.player_missions_asap = settings.auto_ato_player_missions_asap self.default_start_type = settings.default_start_type @@ -133,6 +140,7 @@ class PackageFulfiller: mission.location, ObjectiveDistanceCache.get_closest_airfields(mission.location), self.air_wing, + self.flight_db, self.is_player, self.coalition.country_name, self.default_start_type, diff --git a/game/commander/tasks/packageplanningtask.py b/game/commander/tasks/packageplanningtask.py index d3c9ae35..e515f013 100644 --- a/game/commander/tasks/packageplanningtask.py +++ b/game/commander/tasks/packageplanningtask.py @@ -4,10 +4,12 @@ import itertools import operator from abc import abstractmethod from dataclasses import dataclass, field -from enum import unique, IntEnum, auto -from typing import TYPE_CHECKING, Optional, Generic, TypeVar, Iterator, Union +from enum import IntEnum, auto, unique +from typing import Generic, Iterator, Optional, TYPE_CHECKING, TypeVar, Union -from game.commander.missionproposals import ProposedFlight, EscortType, ProposedMission +from game.ato.flighttype import FlightType +from game.ato.package import Package +from game.commander.missionproposals import EscortType, ProposedFlight, ProposedMission from game.commander.packagefulfiller import PackageFulfiller from game.commander.tasks.theatercommandertask import TheaterCommanderTask from game.commander.theaterstate import TheaterState @@ -15,8 +17,6 @@ from game.settings import AutoAtoBehavior from game.theater import MissionTarget from game.theater.theatergroundobject import IadsGroundObject, NavalGroundObject from game.utils import Distance, meters -from game.ato.package import Package -from game.ato.flighttype import FlightType if TYPE_CHECKING: from game.coalition import Coalition @@ -40,7 +40,6 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]): def __post_init__(self) -> None: self.flights = [] - self.package = Package(self.target) def preconditions_met(self, state: TheaterState) -> bool: if ( @@ -97,6 +96,7 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]): fulfiller = PackageFulfiller( state.context.coalition, state.context.theater, + state.context.game_db.flights, state.context.settings, ) self.package = fulfiller.plan_mission( diff --git a/game/commander/theaterstate.py b/game/commander/theaterstate.py index 9ece973d..0465fe59 100644 --- a/game/commander/theaterstate.py +++ b/game/commander/theaterstate.py @@ -9,6 +9,7 @@ from typing import Any, Optional, TYPE_CHECKING, Union from game.commander.garrisons import Garrisons from game.commander.objectivefinder import ObjectiveFinder +from game.db import GameDb from game.htn import WorldState from game.profiling import MultiEventTracer from game.settings import Settings @@ -31,6 +32,7 @@ if TYPE_CHECKING: @dataclass(frozen=True) class PersistentContext: + game_db: GameDb coalition: Coalition theater: ConflictTheater turn: int @@ -140,7 +142,7 @@ class TheaterState(WorldState["TheaterState"]): ordered_capturable_points = finder.prioritized_unisolated_points() context = PersistentContext( - coalition, game.theater, game.turn, game.settings, tracer + game.db, coalition, game.theater, game.turn, game.settings, tracer ) # Plan enough rounds of CAP that the target has coverage over the expected diff --git a/game/db/__init__.py b/game/db/__init__.py new file mode 100644 index 00000000..86aeff92 --- /dev/null +++ b/game/db/__init__.py @@ -0,0 +1,2 @@ +from .database import Database +from .gamedb import GameDb diff --git a/game/db/database.py b/game/db/database.py new file mode 100644 index 00000000..319f70e9 --- /dev/null +++ b/game/db/database.py @@ -0,0 +1,20 @@ +from typing import Generic, TypeVar +from uuid import UUID + +T = TypeVar("T") + + +class Database(Generic[T]): + def __init__(self) -> None: + self.objects: dict[UUID, T] = {} + + def add(self, uuid: UUID, obj: T) -> None: + if uuid in self.objects: + raise KeyError(f"Object with UUID {uuid} already exists") + self.objects[uuid] = obj + + def get(self, uuid: UUID) -> T: + return self.objects[uuid] + + def remove(self, uuid: UUID) -> None: + del self.objects[uuid] diff --git a/game/db/gamedb.py b/game/db/gamedb.py new file mode 100644 index 00000000..f9e92f1e --- /dev/null +++ b/game/db/gamedb.py @@ -0,0 +1,11 @@ +from typing import TYPE_CHECKING + +from .database import Database + +if TYPE_CHECKING: + from game.ato import Flight + + +class GameDb: + def __init__(self) -> None: + self.flights: Database[Flight] = Database() diff --git a/game/game.py b/game/game.py index 93520b8a..51305c20 100644 --- a/game/game.py +++ b/game/game.py @@ -6,7 +6,7 @@ import math from collections.abc import Iterator from datetime import date, datetime, timedelta from enum import Enum -from typing import Any, List, TYPE_CHECKING, Type, TypeVar, Union, cast +from typing import Any, List, TYPE_CHECKING, Type, Union, cast from dcs.countries import Switzerland, USAFAggressors, UnitedNationsPeacekeepers from dcs.country import Country @@ -25,6 +25,7 @@ from . import persistency from .ato.flighttype import FlightType from .campaignloader import CampaignAirWingConfig from .coalition import Coalition +from .db.gamedb import GameDb from .factions.faction import Faction from .infos.information import Information from .profiling import logged_duration @@ -115,6 +116,8 @@ class Game: self.current_group_id = 0 self.name_generator = naming.namegen + self.db = GameDb() + self.conditions = self.generate_conditions() self.sanitize_sides(player_faction, enemy_faction) diff --git a/game/missiongenerator/aircraft/aircraftgenerator.py b/game/missiongenerator/aircraft/aircraftgenerator.py index a5b1b19f..5439957b 100644 --- a/game/missiongenerator/aircraft/aircraftgenerator.py +++ b/game/missiongenerator/aircraft/aircraftgenerator.py @@ -140,7 +140,7 @@ class AircraftGenerator: # reuse the existing debriefing code. # TODO: Special flight type? flight = Flight( - Package(squadron.location), + Package(squadron.location, self.game.db.flights), faction.country, squadron, 1, diff --git a/game/server/debuggeometries/routes.py b/game/server/debuggeometries/routes.py index 246c590f..d306ce1c 100644 --- a/game/server/debuggeometries/routes.py +++ b/game/server/debuggeometries/routes.py @@ -3,36 +3,22 @@ from uuid import UUID from fastapi import APIRouter, Depends from game import Game -from game.ato import Flight from game.server import GameContext from .models import HoldZonesJs, IpZonesJs, JoinZonesJs router: APIRouter = APIRouter(prefix="/debug/waypoint-geometries") -# TODO: Maintain map of UUID -> Flight in Game. -def find_flight(game: Game, flight_id: UUID) -> Flight: - for coalition in game.coalitions: - for package in coalition.ato.packages: - for flight in package.flights: - if flight.id == flight_id: - return flight - raise KeyError(f"No flight found with ID {flight_id}") - - @router.get("/hold/{flight_id}") def hold_zones(flight_id: UUID, game: Game = Depends(GameContext.get)) -> HoldZonesJs: - flight = find_flight(game, flight_id) - return HoldZonesJs.for_flight(flight, game) + return HoldZonesJs.for_flight(game.db.flights.get(flight_id), game) @router.get("/ip/{flight_id}") def ip_zones(flight_id: UUID, game: Game = Depends(GameContext.get)) -> IpZonesJs: - flight = find_flight(game, flight_id) - return IpZonesJs.for_flight(flight, game) + return IpZonesJs.for_flight(game.db.flights.get(flight_id), game) @router.get("/join/{flight_id}") def join_zones(flight_id: UUID, game: Game = Depends(GameContext.get)) -> JoinZonesJs: - flight = find_flight(game, flight_id) - return JoinZonesJs.for_flight(flight, game) + return JoinZonesJs.for_flight(game.db.flights.get(flight_id), game) diff --git a/game/squadrons/squadron.py b/game/squadrons/squadron.py index 5b978447..d4cf9272 100644 --- a/game/squadrons/squadron.py +++ b/game/squadrons/squadron.py @@ -12,6 +12,7 @@ from game.ato import Flight, FlightType, Package from game.settings import AutoAtoBehavior, Settings from gen.flights.flightplan import FlightPlanBuilder from .pilot import Pilot, PilotStatus +from ..db.database import Database from ..utils import meters if TYPE_CHECKING: @@ -50,6 +51,7 @@ class Squadron: ) coalition: Coalition = field(hash=False, compare=False) + flight_db: Database[Flight] = field(hash=False, compare=False) settings: Settings = field(hash=False, compare=False) location: ControlPoint @@ -388,7 +390,7 @@ class Squadron: if not remaining: return - package = Package(self.destination) + package = Package(self.destination, self.flight_db) builder = FlightPlanBuilder(package, self.coalition, theater) while remaining: size = min(remaining, self.aircraft.max_group_size) @@ -437,6 +439,7 @@ class Squadron: squadron_def.female_pilot_percentage, squadron_def.pilot_pool, coalition, + game.db.flights, game.settings, base, ) diff --git a/game/transfers.py b/game/transfers.py index ee3d0a0f..aa32c32b 100644 --- a/game/transfers.py +++ b/game/transfers.py @@ -36,18 +36,13 @@ import math from collections import defaultdict from dataclasses import dataclass, field from functools import singledispatchmethod -from typing import ( - Generic, - Iterator, - List, - Optional, - TYPE_CHECKING, - TypeVar, - Sequence, -) +from typing import Generic, Iterator, List, Optional, Sequence, TYPE_CHECKING, TypeVar from dcs.mapping import Point +from game.ato.flight import Flight +from game.ato.flighttype import FlightType +from game.ato.package import Package from game.dcs.aircrafttype import AircraftType from game.dcs.groundunittype import GroundUnitType from game.procurement import AircraftProcurementRequest @@ -57,11 +52,8 @@ from game.theater.transitnetwork import ( TransitNetwork, ) from game.utils import meters, nautical_miles -from game.ato.package import Package from gen.flights.ai_flight_planner_db import aircraft_for_task from gen.flights.closestairfields import ObjectiveDistanceCache -from game.ato.flighttype import FlightType -from game.ato.flight import Flight from gen.flights.flightplan import FlightPlanBuilder from gen.naming import namegen @@ -271,7 +263,7 @@ class AirliftPlanner: self.transfer = transfer self.next_stop = next_stop self.for_player = transfer.destination.captured - self.package = Package(target=next_stop, auto_asap=True) + self.package = Package(next_stop, game.db.flights, auto_asap=True) def compatible_with_mission( self, unit_type: AircraftType, airfield: ControlPoint diff --git a/qt_ui/windows/mission/QPackageDialog.py b/qt_ui/windows/mission/QPackageDialog.py index f07f4acd..6d7e2740 100644 --- a/qt_ui/windows/mission/QPackageDialog.py +++ b/qt_ui/windows/mission/QPackageDialog.py @@ -15,10 +15,10 @@ from PySide2.QtWidgets import ( QVBoxLayout, ) +from game.ato.flight import Flight +from game.ato.package import Package from game.game import Game from game.theater.missiontarget import MissionTarget -from game.ato.package import Package -from game.ato.flight import Flight from gen.flights.flightplan import FlightPlanBuilder, PlanningError from qt_ui.models import AtoModel, GameModel, PackageModel from qt_ui.uiconstants import EVENT_ICONS @@ -215,7 +215,9 @@ class QNewPackageDialog(QPackageDialog): ) -> None: super().__init__( game_model, - PackageModel(Package(target, auto_asap=True), game_model), + PackageModel( + Package(target, game_model.game.db.flights, auto_asap=True), game_model + ), parent=parent, ) self.ato_model = model