diff --git a/game/coalition.py b/game/coalition.py new file mode 100644 index 00000000..f7a97ecf --- /dev/null +++ b/game/coalition.py @@ -0,0 +1,215 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Optional + +from dcs import Point +from faker import Faker + +from game.commander import TheaterCommander +from game.income import Income +from game.navmesh import NavMesh +from game.profiling import logged_duration, MultiEventTracer +from game.threatzones import ThreatZones +from game.transfers import PendingTransfers +from gen.flights.ai_flight_planner import CoalitionMissionPlanner + +if TYPE_CHECKING: + from game import Game +from game.data.doctrine import Doctrine +from game.factions.faction import Faction +from game.procurement import AircraftProcurementRequest, ProcurementAi +from game.squadrons import AirWing +from game.theater.bullseye import Bullseye +from game.theater.transitnetwork import TransitNetwork, TransitNetworkBuilder +from gen import AirTaskingOrder + + +class Coalition: + def __init__( + self, game: Game, faction: Faction, budget: float, player: bool + ) -> None: + self.game = game + self.player = player + self.faction = faction + self.budget = budget + self.ato = AirTaskingOrder() + self.transit_network = TransitNetwork() + self.procurement_requests: list[AircraftProcurementRequest] = [] + self.bullseye = Bullseye(Point(0, 0)) + self.faker = Faker(self.faction.locales) + self.air_wing = AirWing(game, self) + self.transfers = PendingTransfers(game, player) + + # Late initialized because the two coalitions in the game are mutually + # dependent, so must be both constructed before this property can be set. + self._opponent: Optional[Coalition] = None + + # Volatile properties that are not persisted to the save file since they can be + # recomputed on load. Keeping this data out of the save file makes save compat + # breaks less frequent. Each of these properties has a non-underscore-prefixed + # @property that should be used for non-Optional access. + # + # All of these are late-initialized (whether via on_load or called later), but + # will be non-None after the game has finished loading. + self._threat_zone: Optional[ThreatZones] = None + self._navmesh: Optional[NavMesh] = None + self.on_load() + + @property + def doctrine(self) -> Doctrine: + return self.faction.doctrine + + @property + def coalition_id(self) -> int: + if self.player: + return 2 + return 1 + + @property + def country_name(self) -> str: + return self.faction.country + + @property + def opponent(self) -> Coalition: + assert self._opponent is not None + return self._opponent + + @property + def threat_zone(self) -> ThreatZones: + assert self._threat_zone is not None + return self._threat_zone + + @property + def nav_mesh(self) -> NavMesh: + assert self._navmesh is not None + return self._navmesh + + def __getstate__(self) -> dict[str, Any]: + state = self.__dict__.copy() + # Avoid persisting any volatile types that can be deterministically + # recomputed on load for the sake of save compatibility. + del state["_threat_zone"] + del state["_navmesh"] + del state["faker"] + return state + + def __setstate__(self, state: dict[str, Any]) -> None: + self.__dict__.update(state) + # Regenerate any state that was not persisted. + self.on_load() + + def on_load(self) -> None: + self.faker = Faker(self.faction.locales) + + def set_opponent(self, opponent: Coalition) -> None: + if self._opponent is not None: + raise RuntimeError("Double-initialization of Coalition.opponent") + self._opponent = opponent + + def adjust_budget(self, amount: float) -> None: + self.budget += amount + + def compute_threat_zones(self) -> None: + self._threat_zone = ThreatZones.for_faction(self.game, self.player) + + def compute_nav_meshes(self) -> None: + self._navmesh = NavMesh.from_threat_zones( + self.opponent.threat_zone, self.game.theater + ) + + def update_transit_network(self) -> None: + self.transit_network = TransitNetworkBuilder( + self.game.theater, self.player + ).build() + + def set_bullseye(self, bullseye: Bullseye) -> None: + self.bullseye = bullseye + + def end_turn(self) -> None: + """Processes coalition-specific turn finalization. + + For more information on turn finalization in general, see the documentation for + `Game.finish_turn`. + """ + self.air_wing.replenish() + self.budget += Income(self.game, self.player).total + + # Need to recompute before transfers and deliveries to account for captures. + # This happens in in initialize_turn as well, because cheating doesn't advance a + # turn but can capture bases so we need to recompute there as well. + self.update_transit_network() + + # Must happen *before* unit deliveries are handled, or else new units will spawn + # one hop ahead. ControlPoint.process_turn handles unit deliveries. The + # coalition-specific turn-end happens before the theater-wide turn-end, so this + # is handled correctly. + self.transfers.perform_transfers() + + def initialize_turn(self) -> None: + """Processes coalition-specific turn initialization. + + For more information on turn initialization in general, see the documentation + for `Game.initialize_turn`. + """ + # Needs to happen *before* planning transfers so we don't cancel them. + self.ato.clear() + self.air_wing.reset() + self.refund_outstanding_orders() + self.procurement_requests.clear() + + with logged_duration("Transit network identification"): + self.update_transit_network() + with logged_duration("Procurement of airlift assets"): + self.transfers.order_airlift_assets() + with logged_duration("Transport planning"): + self.transfers.plan_transports() + + self.plan_missions() + self.plan_procurement() + + def refund_outstanding_orders(self) -> None: + # TODO: Split orders between air and ground units. + # This isn't quite right. If the player has ground purchases automated we should + # be refunding the ground units, and if they have air automated but not ground + # we should be refunding air units. + if self.player and not self.game.settings.automate_aircraft_reinforcements: + return + + for cp in self.game.theater.control_points_for(self.player): + cp.pending_unit_deliveries.refund_all(self) + + def plan_missions(self) -> None: + color = "Blue" if self.player else "Red" + with MultiEventTracer() as tracer: + mission_planner = CoalitionMissionPlanner(self.game, self.player) + with tracer.trace(f"{color} mission planning"): + with tracer.trace(f"{color} mission identification"): + commander = TheaterCommander(self.game, self.player) + commander.plan_missions(mission_planner, tracer) + with tracer.trace(f"{color} mission fulfillment"): + mission_planner.fulfill_missions() + + def plan_procurement(self) -> None: + # The first turn needs to buy a *lot* of aircraft to fill CAPs, so it gets much + # more of the budget that turn. Otherwise budget (after repairs) is split evenly + # between air and ground. For the default starting budget of 2000 this gives 600 + # to ground forces and 1400 to aircraft. After that the budget will be spent + # proportionally based on how much is already invested. + + if self.player: + manage_runways = self.game.settings.automate_runway_repair + manage_front_line = self.game.settings.automate_front_line_reinforcements + manage_aircraft = self.game.settings.automate_aircraft_reinforcements + else: + manage_runways = False + manage_front_line = False + manage_aircraft = False + + self.budget = ProcurementAi( + self.game, + self.player, + self.faction, + manage_runways, + manage_front_line, + manage_aircraft, + ).spend_budget(self.budget) diff --git a/game/commander/objectivefinder.py b/game/commander/objectivefinder.py index 34db8730..15e47ed0 100644 --- a/game/commander/objectivefinder.py +++ b/game/commander/objectivefinder.py @@ -14,9 +14,9 @@ from game.theater import ( Airfield, ) from game.theater.theatergroundobject import ( - NavalGroundObject, BuildingGroundObject, IadsGroundObject, + NavalGroundObject, ) from game.transfers import CargoShip, Convoy from game.utils import meters, nautical_miles @@ -163,13 +163,17 @@ class ObjectiveFinder: def convoys(self) -> Iterator[Convoy]: for front_line in self.front_lines(): - yield from self.game.transfers.convoys.travelling_to( + yield from self.game.coalition_for( + self.is_player + ).transfers.convoys.travelling_to( front_line.control_point_hostile_to(self.is_player) ) def cargo_ships(self) -> Iterator[CargoShip]: for front_line in self.front_lines(): - yield from self.game.transfers.cargo_ships.travelling_to( + yield from self.game.coalition_for( + self.is_player + ).transfers.cargo_ships.travelling_to( front_line.control_point_hostile_to(self.is_player) ) diff --git a/game/commander/tasks/primitive/antiship.py b/game/commander/tasks/primitive/antiship.py index fcdc2273..10a55477 100644 --- a/game/commander/tasks/primitive/antiship.py +++ b/game/commander/tasks/primitive/antiship.py @@ -6,7 +6,7 @@ from game.commander.missionproposals import EscortType from game.commander.tasks.packageplanningtask import PackagePlanningTask from game.commander.theaterstate import TheaterState from game.data.doctrine import Doctrine -from game.theater import NavalGroundObject +from game.theater.theatergroundobject import NavalGroundObject from gen.flights.flight import FlightType diff --git a/game/debriefing.py b/game/debriefing.py index e4e3bf0f..b2a155ad 100644 --- a/game/debriefing.py +++ b/game/debriefing.py @@ -18,7 +18,6 @@ from typing import ( Union, ) -from game import db from game.dcs.aircrafttype import AircraftType from game.dcs.groundunittype import GroundUnitType from game.theater import Airfield, ControlPoint @@ -136,10 +135,8 @@ class Debriefing: self.game = game self.unit_map = unit_map - self.player_country = game.player_country - self.enemy_country = game.enemy_country - self.player_country_id = db.country_id_from_name(game.player_country) - self.enemy_country_id = db.country_id_from_name(game.enemy_country) + self.player_country = game.blue.country_name + self.enemy_country = game.red.country_name self.air_losses = self.dead_aircraft() self.ground_losses = self.dead_ground_units() diff --git a/game/event/event.py b/game/event/event.py index ad20e06c..2176220c 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -53,7 +53,7 @@ class Event: @property def is_player_attacking(self) -> bool: - return self.attacker_name == self.game.player_faction.name + return self.attacker_name == self.game.blue.faction.name @property def tasks(self) -> List[Type[Task]]: @@ -114,10 +114,10 @@ class Event: def complete_aircraft_transfers(self, debriefing: Debriefing) -> None: self._transfer_aircraft( - self.game.blue_ato, debriefing.air_losses, for_player=True + self.game.blue.ato, debriefing.air_losses, for_player=True ) self._transfer_aircraft( - self.game.red_ato, debriefing.air_losses, for_player=False + self.game.red.ato, debriefing.air_losses, for_player=False ) def commit_air_losses(self, debriefing: Debriefing) -> None: @@ -154,8 +154,8 @@ class Event: pilot.record.missions_flown += 1 def commit_pilot_experience(self) -> None: - self._commit_pilot_experience(self.game.blue_ato) - self._commit_pilot_experience(self.game.red_ato) + self._commit_pilot_experience(self.game.blue.ato) + self._commit_pilot_experience(self.game.red.ato) @staticmethod def commit_front_line_losses(debriefing: Debriefing) -> None: diff --git a/game/game.py b/game/game.py index fce069f8..9e1cb00c 100644 --- a/game/game.py +++ b/game/game.py @@ -1,13 +1,11 @@ import itertools import logging import math -import random -import sys +from collections import Iterator from datetime import date, datetime, timedelta from enum import Enum from typing import Any, List, Type, Union, cast -from dcs.action import Coalition from dcs.mapping import Point from dcs.task import CAP, CAS, PinpointStrike from dcs.vehicles import AirDefence @@ -19,28 +17,25 @@ from game.plugins import LuaPluginManager from gen import naming from gen.ato import AirTaskingOrder from gen.conflictgen import Conflict -from gen.flights.ai_flight_planner import CoalitionMissionPlanner from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.flight import FlightType from gen.ground_forces.ai_ground_planner import GroundPlanner from . import persistency -from .commander import TheaterCommander +from .coalition import Coalition from .debriefing import Debriefing from .event.event import Event from .event.frontlineattack import FrontlineAttackEvent from .factions.faction import Faction -from .income import Income from .infos.information import Information from .navmesh import NavMesh -from .procurement import AircraftProcurementRequest, ProcurementAi -from .profiling import logged_duration, MultiEventTracer -from .settings import Settings, AutoAtoBehavior +from .procurement import AircraftProcurementRequest +from .profiling import logged_duration +from .settings import Settings from .squadrons import AirWing from .theater import ConflictTheater, ControlPoint from .theater.bullseye import Bullseye from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder from .threatzones import ThreatZones -from .transfers import PendingTransfers from .unitmap import UnitMap from .weather import Conditions, TimeOfDay @@ -98,10 +93,6 @@ class Game: self.settings = settings self.events: List[Event] = [] self.theater = theater - self.player_faction = player_faction - self.player_country = player_faction.country - self.enemy_faction = enemy_faction - self.enemy_country = enemy_faction.country # pass_turn() will be called when initialization is complete which will # increment this to turn 0 before it reaches the player. self.turn = -1 @@ -124,108 +115,70 @@ class Game: self.conditions = self.generate_conditions() - self.blue_transit_network = TransitNetwork() - self.red_transit_network = TransitNetwork() - - self.blue_procurement_requests: List[AircraftProcurementRequest] = [] - self.red_procurement_requests: List[AircraftProcurementRequest] = [] - - self.blue_ato = AirTaskingOrder() - self.red_ato = AirTaskingOrder() - - self.blue_bullseye = Bullseye(Point(0, 0)) - self.red_bullseye = Bullseye(Point(0, 0)) + self.sanitize_sides(player_faction, enemy_faction) + self.blue = Coalition(self, player_faction, player_budget, player=True) + self.red = Coalition(self, enemy_faction, enemy_budget, player=False) + self.blue.set_opponent(self.red) + self.red.set_opponent(self.blue) self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints) - self.transfers = PendingTransfers(self) - - self.sanitize_sides() - - self.blue_faker = Faker(self.player_faction.locales) - self.red_faker = Faker(self.enemy_faction.locales) - - self.blue_air_wing = AirWing(self, player=True) - self.red_air_wing = AirWing(self, player=False) - self.on_load(game_still_initializing=True) - def __getstate__(self) -> dict[str, Any]: - state = self.__dict__.copy() - # Avoid persisting any volatile types that can be deterministically - # recomputed on load for the sake of save compatibility. - del state["blue_threat_zone"] - del state["red_threat_zone"] - del state["blue_navmesh"] - del state["red_navmesh"] - del state["blue_faker"] - del state["red_faker"] - return state - def __setstate__(self, state: dict[str, Any]) -> None: self.__dict__.update(state) # Regenerate any state that was not persisted. self.on_load() + @property + def coalitions(self) -> Iterator[Coalition]: + yield self.blue + yield self.red + def ato_for(self, player: bool) -> AirTaskingOrder: - if player: - return self.blue_ato - return self.red_ato + return self.coalition_for(player).ato def procurement_requests_for( self, player: bool - ) -> List[AircraftProcurementRequest]: - if player: - return self.blue_procurement_requests - return self.red_procurement_requests + ) -> list[AircraftProcurementRequest]: + return self.coalition_for(player).procurement_requests def transit_network_for(self, player: bool) -> TransitNetwork: - if player: - return self.blue_transit_network - return self.red_transit_network + return self.coalition_for(player).transit_network def generate_conditions(self) -> Conditions: return Conditions.generate( self.theater, self.current_day, self.current_turn_time_of_day, self.settings ) - def sanitize_sides(self) -> None: + @staticmethod + def sanitize_sides(player_faction: Faction, enemy_faction: Faction) -> None: """ Make sure the opposing factions are using different countries :return: """ - if self.player_country == self.enemy_country: - if self.player_country == "USA": - self.enemy_country = "USAF Aggressors" - elif self.player_country == "Russia": - self.enemy_country = "USSR" + if player_faction.country == enemy_faction.country: + if player_faction.country == "USA": + enemy_faction.country = "USAF Aggressors" + elif player_faction.country == "Russia": + enemy_faction.country = "USSR" else: - self.enemy_country = "Russia" + enemy_faction.country = "Russia" def faction_for(self, player: bool) -> Faction: - if player: - return self.player_faction - return self.enemy_faction + return self.coalition_for(player).faction def faker_for(self, player: bool) -> Faker: - if player: - return self.blue_faker - return self.red_faker + return self.coalition_for(player).faker def air_wing_for(self, player: bool) -> AirWing: - if player: - return self.blue_air_wing - return self.red_air_wing + return self.coalition_for(player).air_wing def country_for(self, player: bool) -> str: - if player: - return self.player_country - return self.enemy_country + return self.coalition_for(player).country_name def bullseye_for(self, player: bool) -> Bullseye: - if player: - return self.blue_bullseye - return self.red_bullseye + return self.coalition_for(player).bullseye def _generate_player_event( self, event_class: Type[Event], player_cp: ControlPoint, enemy_cp: ControlPoint @@ -236,8 +189,8 @@ class Game: player_cp, enemy_cp, enemy_cp.position, - self.player_faction.name, - self.enemy_faction.name, + self.blue.faction.name, + self.red.faction.name, ) ) @@ -249,20 +202,13 @@ class Game: front_line.red_cp, ) - def adjust_budget(self, amount: float, player: bool) -> None: + def coalition_for(self, player: bool) -> Coalition: if player: - self.budget += amount - else: - self.enemy_budget += amount + return self.blue + return self.red - def process_player_income(self) -> None: - self.budget += Income(self, player=True).total - - def process_enemy_income(self) -> None: - # TODO: Clean up save compat. - if not hasattr(self, "enemy_budget"): - self.enemy_budget = 0 - self.enemy_budget += Income(self, player=False).total + def adjust_budget(self, amount: float, player: bool) -> None: + self.coalition_for(player).adjust_budget(amount) @staticmethod def initiate_event(event: Event) -> UnitMap: @@ -293,12 +239,6 @@ class Game: self.compute_conflicts_position() if not game_still_initializing: self.compute_threat_zones() - self.blue_faker = Faker(self.faction_for(player=True).locales) - self.red_faker = Faker(self.faction_for(player=False).locales) - - def reset_ato(self) -> None: - self.blue_ato.clear() - self.red_ato.clear() def finish_turn(self, skipped: bool = False) -> None: """Finalizes the current turn and advances to the next turn. @@ -333,23 +273,16 @@ class Game: ) self.turn += 1 - # Need to recompute before transfers and deliveries to account for captures. - # This happens in in initialize_turn as well, because cheating doesn't advance a - # turn but can capture bases so we need to recompute there as well. - self.compute_transit_networks() + # The coalition-specific turn finalization *must* happen before unit deliveries, + # since the coalition-specific finalization handles transit network updates and + # transfer processing. If in the other order, units may be delivered to captured + # bases, and freshly delivered units will spawn one leg through their journey. + self.blue.end_turn() + self.red.end_turn() - # Must happen *before* unit deliveries are handled, or else new units will spawn - # one hop ahead. ControlPoint.process_turn handles unit deliveries. - self.transfers.perform_transfers() - - # Needs to happen *before* planning transfers so we don't cancel them. - self.reset_ato() for control_point in self.theater.controlpoints: control_point.process_turn(self) - self.blue_air_wing.replenish() - self.red_air_wing.replenish() - if not skipped: for cp in self.theater.player_points(): cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY) @@ -360,9 +293,6 @@ class Game: self.conditions = self.generate_conditions() - self.process_enemy_income() - self.process_player_income() - def begin_turn_0(self) -> None: """Initialization for the first turn of the game.""" self.turn = 0 @@ -402,8 +332,8 @@ class Game: def set_bullseye(self) -> None: player_cp, enemy_cp = self.theater.closest_opposing_control_points() - self.blue_bullseye = Bullseye(enemy_cp.position) - self.red_bullseye = Bullseye(player_cp.position) + self.blue.bullseye = Bullseye(enemy_cp.position) + self.red.bullseye = Bullseye(player_cp.position) def initialize_turn(self, for_red: bool = True, for_blue: bool = True) -> None: """Performs turn initialization for the specified players. @@ -451,13 +381,20 @@ class Game: if turn_state in (TurnState.LOSS, TurnState.WIN): return self.process_win_loss(turn_state) + # Plan flights & combat for next turn + with logged_duration("Computing conflict positions"): + self.compute_conflicts_position() + with logged_duration("Threat zone computation"): + self.compute_threat_zones() + # Plan Coalition specific turn - if for_red: - self.initialize_turn_for(player=False) if for_blue: self.initialize_turn_for(player=True) + if for_red: + self.initialize_turn_for(player=False) # Plan GroundWar + self.ground_planners = {} for cp in self.theater.controlpoints: if cp.has_frontline: gplanner = GroundPlanner(cp, self) @@ -465,83 +402,10 @@ class Game: self.ground_planners[cp.id] = gplanner def initialize_turn_for(self, player: bool) -> None: - """Processes coalition-specific turn initialization. - - For more information on turn initialization in general, see the documentation - for `Game.initialize_turn`. - - Args: - player: True if the player coalition is being initialized. False for opfor - initialization. - """ - self.ato_for(player).clear() - self.air_wing_for(player).reset() - - self.aircraft_inventory.reset() - for cp in self.theater.controlpoints: + self.aircraft_inventory.reset(player) + for cp in self.theater.control_points_for(player): self.aircraft_inventory.set_from_control_point(cp) - # Refund all pending deliveries for opfor and if player - # has automate_aircraft_reinforcements - if (not player and not cp.captured) or ( - player - and cp.captured - and self.settings.automate_aircraft_reinforcements - ): - cp.pending_unit_deliveries.refund_all(self) - - # Plan flights & combat for next turn - with logged_duration("Computing conflict positions"): - self.compute_conflicts_position() - with logged_duration("Threat zone computation"): - self.compute_threat_zones() - with logged_duration("Transit network identification"): - self.compute_transit_networks() - self.ground_planners = {} - - self.procurement_requests_for(player).clear() - - with logged_duration("Procurement of airlift assets"): - self.transfers.order_airlift_assets() - with logged_duration("Transport planning"): - self.transfers.plan_transports() - - color = "Blue" if player else "Red" - with MultiEventTracer() as tracer: - mission_planner = CoalitionMissionPlanner(self, player) - with tracer.trace(f"{color} mission planning"): - with tracer.trace(f"{color} mission identification"): - commander = TheaterCommander(self, player) - commander.plan_missions(mission_planner, tracer) - with tracer.trace(f"{color} mission fulfillment"): - mission_planner.fulfill_missions() - - self.plan_procurement_for(player) - - def plan_procurement_for(self, for_player: bool) -> None: - # The first turn needs to buy a *lot* of aircraft to fill CAPs, so it - # gets much more of the budget that turn. Otherwise budget (after - # repairs) is split evenly between air and ground. For the default - # starting budget of 2000 this gives 600 to ground forces and 1400 to - # aircraft. After that the budget will be spend proportionally based on how much is already invested - - if for_player: - self.budget = ProcurementAi( - self, - for_player=True, - faction=self.player_faction, - manage_runways=self.settings.automate_runway_repair, - manage_front_line=self.settings.automate_front_line_reinforcements, - manage_aircraft=self.settings.automate_aircraft_reinforcements, - ).spend_budget(self.budget) - else: - self.enemy_budget = ProcurementAi( - self, - for_player=False, - faction=self.enemy_faction, - manage_runways=True, - manage_front_line=True, - manage_aircraft=True, - ).spend_budget(self.enemy_budget) + self.coalition_for(player).initialize_turn() def message(self, text: str) -> None: self.informations.append(Information(text, turn=self.turn)) @@ -568,32 +432,20 @@ class Game: self.current_group_id += 1 return self.current_group_id - def compute_transit_networks(self) -> None: - self.blue_transit_network = self.compute_transit_network_for(player=True) - self.red_transit_network = self.compute_transit_network_for(player=False) - def compute_transit_network_for(self, player: bool) -> TransitNetwork: return TransitNetworkBuilder(self.theater, player).build() def compute_threat_zones(self) -> None: - self.blue_threat_zone = ThreatZones.for_faction(self, player=True) - self.red_threat_zone = ThreatZones.for_faction(self, player=False) - self.blue_navmesh = NavMesh.from_threat_zones( - self.red_threat_zone, self.theater - ) - self.red_navmesh = NavMesh.from_threat_zones( - self.blue_threat_zone, self.theater - ) + self.blue.compute_threat_zones() + self.red.compute_threat_zones() + self.blue.compute_nav_meshes() + self.red.compute_nav_meshes() def threat_zone_for(self, player: bool) -> ThreatZones: - if player: - return self.blue_threat_zone - return self.red_threat_zone + return self.coalition_for(player).threat_zone def navmesh_for(self, player: bool) -> NavMesh: - if player: - return self.blue_navmesh - return self.red_navmesh + return self.coalition_for(player).nav_mesh def compute_conflicts_position(self) -> None: """ @@ -636,7 +488,7 @@ class Game: if cpoint is not None: zones.append(cpoint) - packages = itertools.chain(self.blue_ato.packages, self.red_ato.packages) + packages = itertools.chain(self.blue.ato.packages, self.red.ato.packages) for package in packages: if package.primary_task is FlightType.BARCAP: # BARCAPs will be planned at most locations on smaller theaters, @@ -682,25 +534,6 @@ class Game: """ return self.__culling_zones - # 1 = red, 2 = blue - def get_player_coalition_id(self) -> int: - return 2 - - def get_enemy_coalition_id(self) -> int: - return 1 - - def get_player_coalition(self) -> Coalition: - return Coalition.Blue - - def get_enemy_coalition(self) -> Coalition: - return Coalition.Red - - def get_player_color(self) -> str: - return "blue" - - def get_enemy_color(self) -> str: - return "red" - def process_win_loss(self, turn_state: TurnState) -> None: if turn_state is TurnState.WIN: self.message( diff --git a/game/inventory.py b/game/inventory.py index 4014c05c..77587cb2 100644 --- a/game/inventory.py +++ b/game/inventory.py @@ -2,9 +2,7 @@ from __future__ import annotations from collections import defaultdict -from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING, Type - -from dcs.unittype import FlyingType +from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING from game.dcs.aircrafttype import AircraftType from gen.flights.flight import Flight @@ -86,10 +84,11 @@ class GlobalAircraftInventory: cp: ControlPointAircraftInventory(cp) for cp in control_points } - def reset(self) -> None: - """Clears all control points and their inventories.""" + def reset(self, for_player: bool) -> None: + """Clears the inventory of every control point owned by the given coalition.""" for inventory in self.inventories.values(): - inventory.clear() + if inventory.control_point.captured == for_player: + inventory.clear() def set_from_control_point(self, control_point: ControlPoint) -> None: """Set the control point's aircraft inventory. diff --git a/game/operation/operation.py b/game/operation/operation.py index 56cfcf66..290076db 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging import os from pathlib import Path -from typing import Iterable, List, Set, TYPE_CHECKING, cast +from typing import List, Set, TYPE_CHECKING, cast from dcs import Mission from dcs.action import DoScript, DoScriptFile @@ -81,10 +81,10 @@ class Operation: return Conflict( cls.game.theater, FrontLine(player_cp, enemy_cp), - cls.game.player_faction.name, - cls.game.enemy_faction.name, - cls.current_mission.country(cls.game.player_country), - cls.current_mission.country(cls.game.enemy_country), + cls.game.blue.faction.name, + cls.game.red.faction.name, + cls.current_mission.country(cls.game.blue.country_name), + cls.current_mission.country(cls.game.red.country_name), mid_point, ) @@ -95,14 +95,14 @@ class Operation: @classmethod def _setup_mission_coalitions(cls) -> None: cls.current_mission.coalition["blue"] = Coalition( - "blue", bullseye=cls.game.blue_bullseye.to_pydcs() + "blue", bullseye=cls.game.blue.bullseye.to_pydcs() ) cls.current_mission.coalition["red"] = Coalition( - "red", bullseye=cls.game.red_bullseye.to_pydcs() + "red", bullseye=cls.game.red.bullseye.to_pydcs() ) - p_country = cls.game.player_country - e_country = cls.game.enemy_country + p_country = cls.game.blue.country_name + e_country = cls.game.red.country_name cls.current_mission.coalition["blue"].add_country( country_dict[db.country_id_from_name(p_country)]() ) @@ -268,7 +268,7 @@ class Operation: and cls.game.settings.perf_destroyed_units ): cls.current_mission.static_group( - country=cls.current_mission.country(cls.game.player_country), + country=cls.current_mission.country(cls.game.blue.country_name), name="", _type=utype, hidden=True, @@ -358,18 +358,18 @@ class Operation: cls.airgen.clear_parking_slots() cls.airgen.generate_flights( - cls.current_mission.country(cls.game.player_country), - cls.game.blue_ato, + cls.current_mission.country(cls.game.blue.country_name), + cls.game.blue.ato, cls.groundobjectgen.runways, ) cls.airgen.generate_flights( - cls.current_mission.country(cls.game.enemy_country), - cls.game.red_ato, + cls.current_mission.country(cls.game.red.country_name), + cls.game.red.ato, cls.groundobjectgen.runways, ) cls.airgen.spawn_unused_aircraft( - cls.current_mission.country(cls.game.player_country), - cls.current_mission.country(cls.game.enemy_country), + cls.current_mission.country(cls.game.blue.country_name), + cls.current_mission.country(cls.game.red.country_name), ) @classmethod @@ -380,10 +380,10 @@ class Operation: player_cp = front_line.blue_cp enemy_cp = front_line.red_cp conflict = Conflict.frontline_cas_conflict( - cls.game.player_faction.name, - cls.game.enemy_faction.name, - cls.current_mission.country(cls.game.player_country), - cls.current_mission.country(cls.game.enemy_country), + cls.game.blue.faction.name, + cls.game.red.faction.name, + cls.current_mission.country(cls.game.blue.country_name), + cls.current_mission.country(cls.game.red.country_name), front_line, cls.game.theater, ) diff --git a/game/procurement.py b/game/procurement.py index 2e2c0e79..8820453c 100644 --- a/game/procurement.py +++ b/game/procurement.py @@ -72,7 +72,9 @@ class ProcurementAi: return 1 for cp in self.owned_points: - cp_ground_units = cp.allocated_ground_units(self.game.transfers) + cp_ground_units = cp.allocated_ground_units( + self.game.coalition_for(self.is_player).transfers + ) armor_investment += cp_ground_units.total_value cp_aircraft = cp.allocated_aircraft(self.game) aircraft_investment += cp_aircraft.total_value @@ -316,7 +318,9 @@ class ProcurementAi: continue purchase_target = cp.frontline_unit_count_limit * FRONTLINE_RESERVES_FACTOR - allocated = cp.allocated_ground_units(self.game.transfers) + allocated = cp.allocated_ground_units( + self.game.coalition_for(self.is_player).transfers + ) if allocated.total >= purchase_target: # Control point is already sufficiently defended. continue @@ -343,7 +347,9 @@ class ProcurementAi: if not cp.can_recruit_ground_units(self.game): continue - allocated = cp.allocated_ground_units(self.game.transfers) + allocated = cp.allocated_ground_units( + self.game.coalition_for(self.is_player).transfers + ) if allocated.total >= self.game.settings.reserves_procurement_target: continue @@ -356,7 +362,9 @@ class ProcurementAi: def cost_ratio_of_ground_unit( self, control_point: ControlPoint, unit_class: GroundUnitClass ) -> float: - allocations = control_point.allocated_ground_units(self.game.transfers) + allocations = control_point.allocated_ground_units( + self.game.coalition_for(self.is_player).transfers + ) class_cost = 0 total_cost = 0 for unit_type, count in allocations.all.items(): diff --git a/game/squadrons.py b/game/squadrons.py index f3793916..3a23d4ea 100644 --- a/game/squadrons.py +++ b/game/squadrons.py @@ -20,10 +20,11 @@ import yaml from faker import Faker from game.dcs.aircrafttype import AircraftType -from game.settings import AutoAtoBehavior +from game.settings import AutoAtoBehavior, Settings if TYPE_CHECKING: from game import Game + from game.coalition import Coalition from gen.flights.flight import FlightType @@ -96,16 +97,13 @@ class Squadron: init=False, hash=False, compare=False ) - # We need a reference to the Game so that we can access the Faker without needing to - # persist it to the save game, or having to reconstruct it (it's not cheap) each - # time we create or load a squadron. - game: Game = field(hash=False, compare=False) - player: bool + coalition: Coalition = field(hash=False, compare=False) + settings: Settings = field(hash=False, compare=False) def __post_init__(self) -> None: if any(p.status is not PilotStatus.Active for p in self.pilot_pool): raise ValueError("Squadrons can only be created with active pilots.") - self._recruit_pilots(self.game.settings.squadron_pilot_limit) + self._recruit_pilots(self.settings.squadron_pilot_limit) self.auto_assignable_mission_types = set(self.mission_types) def __str__(self) -> str: @@ -113,9 +111,13 @@ class Squadron: return self.name return f'{self.name} "{self.nickname}"' + @property + def player(self) -> bool: + return self.coalition.player + @property def pilot_limits_enabled(self) -> bool: - return self.game.settings.enable_squadron_pilot_limits + return self.settings.enable_squadron_pilot_limits def claim_new_pilot_if_allowed(self) -> Optional[Pilot]: if self.pilot_limits_enabled: @@ -131,7 +133,7 @@ class Squadron: if not self.player: return self.available_pilots.pop() - preference = self.game.settings.auto_ato_behavior + preference = self.settings.auto_ato_behavior # No preference, so the first pilot is fine. if preference is AutoAtoBehavior.Default: @@ -184,7 +186,7 @@ class Squadron: return replenish_count = min( - self.game.settings.squadron_replenishment_rate, + self.settings.squadron_replenishment_rate, self._number_of_unfilled_pilot_slots, ) if replenish_count > 0: @@ -206,7 +208,7 @@ class Squadron: @property def faker(self) -> Faker: - return self.game.faker_for(self.player) + return self.coalition.faker def _pilots_with_status(self, status: PilotStatus) -> list[Pilot]: return [p for p in self.current_roster if p.status == status] @@ -228,7 +230,7 @@ class Squadron: @property def _number_of_unfilled_pilot_slots(self) -> int: - return self.game.settings.squadron_pilot_limit - len(self.active_pilots) + return self.settings.squadron_pilot_limit - len(self.active_pilots) @property def number_of_available_pilots(self) -> int: @@ -252,7 +254,7 @@ class Squadron: return self.current_roster[index] @classmethod - def from_yaml(cls, path: Path, game: Game, player: bool) -> Squadron: + def from_yaml(cls, path: Path, game: Game, coalition: Coalition) -> Squadron: from gen.flights.ai_flight_planner_db import tasks_for_aircraft from gen.flights.flight import FlightType @@ -287,8 +289,8 @@ class Squadron: livery=data.get("livery"), mission_types=tuple(mission_types), pilot_pool=pilots, - game=game, - player=player, + coalition=coalition, + settings=game.settings, ) def __setstate__(self, state: dict[str, Any]) -> None: @@ -299,9 +301,9 @@ class Squadron: class SquadronLoader: - def __init__(self, game: Game, player: bool) -> None: + def __init__(self, game: Game, coalition: Coalition) -> None: self.game = game - self.player = player + self.coalition = coalition @staticmethod def squadron_directories() -> Iterator[Path]: @@ -312,8 +314,8 @@ class SquadronLoader: def load(self) -> dict[AircraftType, list[Squadron]]: squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list) - country = self.game.country_for(self.player) - faction = self.game.faction_for(self.player) + country = self.coalition.country_name + faction = self.coalition.faction any_country = country.startswith("Combined Joint Task Forces ") for directory in self.squadron_directories(): for path, squadron in self.load_squadrons_from(directory): @@ -347,7 +349,7 @@ class SquadronLoader: for squadron_path in directory.glob("*/*.yaml"): try: yield squadron_path, Squadron.from_yaml( - squadron_path, self.game, self.player + squadron_path, self.game, self.coalition ) except Exception as ex: raise RuntimeError( @@ -356,29 +358,28 @@ class SquadronLoader: class AirWing: - def __init__(self, game: Game, player: bool) -> None: + def __init__(self, game: Game, coalition: Coalition) -> None: from gen.flights.ai_flight_planner_db import tasks_for_aircraft self.game = game - self.player = player - self.squadrons = SquadronLoader(game, player).load() + self.squadrons = SquadronLoader(game, coalition).load() count = itertools.count(1) - for aircraft in game.faction_for(player).aircrafts: + for aircraft in coalition.faction.aircrafts: if aircraft in self.squadrons: continue self.squadrons[aircraft] = [ Squadron( name=f"Squadron {next(count):03}", nickname=self.random_nickname(), - country=game.country_for(player), + country=coalition.country_name, role="Flying Squadron", aircraft=aircraft, livery=None, mission_types=tuple(tasks_for_aircraft(aircraft)), pilot_pool=[], - game=game, - player=player, + coalition=coalition, + settings=game.settings, ) ] diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index e1333dfc..7fedc7fe 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -40,11 +40,7 @@ from gen.ground_forces.combat_stance import CombatStance from gen.runways import RunwayAssigner, RunwayData from .base import Base from .missiontarget import MissionTarget -from .theatergroundobject import ( - GenericCarrierGroundObject, - TheaterGroundObject, - NavalGroundObject, -) +from .theatergroundobject import GenericCarrierGroundObject, TheaterGroundObject from ..dcs.aircrafttype import AircraftType from ..dcs.groundunittype import GroundUnitType from ..utils import nautical_miles @@ -606,7 +602,7 @@ class ControlPoint(MissionTarget, ABC): # TODO: Should be Airbase specific. def capture(self, game: Game, for_player: bool) -> None: - self.pending_unit_deliveries.refund_all(game) + self.pending_unit_deliveries.refund_all(game.coalition_for(for_player)) self.retreat_ground_units(game) self.retreat_air_units(game) self.depopulate_uncapturable_tgos() @@ -623,11 +619,7 @@ class ControlPoint(MissionTarget, ABC): ... def aircraft_transferring(self, game: Game) -> dict[AircraftType, int]: - if self.captured: - ato = game.blue_ato - else: - ato = game.red_ato - + ato = game.coalition_for(self.captured).ato transferring: defaultdict[AircraftType, int] = defaultdict(int) for package in ato.packages: for flight in package.flights: diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index 4ec827ec..0bf85391 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -11,7 +11,7 @@ from dcs.mapping import Point from dcs.task import CAP, CAS, PinpointStrike from dcs.vehicles import AirDefence -from game import Game, db +from game import Game from game.factions.faction import Faction from game.scenery_group import SceneryGroup from game.theater import Carrier, Lha, PointWithHeading @@ -171,14 +171,11 @@ class ControlPointGroundObjectGenerator: @property def faction_name(self) -> str: - if self.control_point.captured: - return self.game.player_faction.name - else: - return self.game.enemy_faction.name + return self.faction.name @property def faction(self) -> Faction: - return db.FACTIONS[self.faction_name] + return self.game.coalition_for(self.control_point.captured).faction def generate(self) -> bool: self.control_point.connected_objectives = [] diff --git a/game/transfers.py b/game/transfers.py index a575964f..3bdc9b3c 100644 --- a/game/transfers.py +++ b/game/transfers.py @@ -316,7 +316,9 @@ class AirliftPlanner: capacity = flight_size * capacity_each if capacity < self.transfer.size: - transfer = self.game.transfers.split_transfer(self.transfer, capacity) + transfer = self.game.coalition_for( + self.for_player + ).transfers.split_transfer(self.transfer, capacity) else: transfer = self.transfer @@ -534,8 +536,9 @@ class CargoShipMap(TransportMap[CargoShip]): class PendingTransfers: - def __init__(self, game: Game) -> None: + def __init__(self, game: Game, player: bool) -> None: self.game = game + self.player = player self.convoys = ConvoyMap() self.cargo_ships = CargoShipMap() self.pending_transfers: List[TransferOrder] = [] @@ -609,7 +612,7 @@ class PendingTransfers: flight = transport.flight flight.package.remove_flight(flight) if not flight.package.flights: - self.game.ato_for(transport.player_owned).remove_package(flight.package) + self.game.ato_for(self.player).remove_package(flight.package) self.game.aircraft_inventory.return_from_flight(flight) flight.clear_roster() @@ -647,7 +650,7 @@ class PendingTransfers: self.arrange_transport(transfer) def order_airlift_assets(self) -> None: - for control_point in self.game.theater.controlpoints: + for control_point in self.game.theater.control_points_for(self.player): if self.game.air_wing_for(control_point.captured).can_auto_plan( FlightType.TRANSPORT ): @@ -682,7 +685,7 @@ class PendingTransfers: # aesthetic. gap += 1 - self.game.procurement_requests_for(player=control_point.captured).append( + self.game.procurement_requests_for(self.player).append( AircraftProcurementRequest( control_point, nautical_miles(200), FlightType.TRANSPORT, gap ) diff --git a/game/unitdelivery.py b/game/unitdelivery.py index ff7841c6..7dbfb0a0 100644 --- a/game/unitdelivery.py +++ b/game/unitdelivery.py @@ -6,6 +6,7 @@ from dataclasses import dataclass from typing import Optional, TYPE_CHECKING, Any from game.theater import ControlPoint +from .coalition import Coalition from .dcs.groundunittype import GroundUnitType from .dcs.unittype import UnitType from .theater.transitnetwork import ( @@ -41,24 +42,22 @@ class PendingUnitDeliveries: for k, v in units.items(): self.units[k] -= v - def refund_all(self, game: Game) -> None: - self.refund(game, self.units) + def refund_all(self, coalition: Coalition) -> None: + self.refund(coalition, self.units) self.units = defaultdict(int) - def refund_ground_units(self, game: Game) -> None: + def refund_ground_units(self, coalition: Coalition) -> None: ground_units: dict[UnitType[Any], int] = { u: self.units[u] for u in self.units.keys() if isinstance(u, GroundUnitType) } - self.refund(game, ground_units) + self.refund(coalition, ground_units) for gu in ground_units.keys(): del self.units[gu] - def refund(self, game: Game, units: dict[UnitType[Any], int]) -> None: + def refund(self, coalition: Coalition, units: dict[UnitType[Any], int]) -> None: for unit_type, count in units.items(): logging.info(f"Refunding {count} {unit_type} at {self.destination.name}") - game.adjust_budget( - unit_type.price * count, player=self.destination.captured - ) + coalition.adjust_budget(unit_type.price * count) def pending_orders(self, unit_type: UnitType[Any]) -> int: pending_units = self.units.get(unit_type) @@ -71,19 +70,20 @@ class PendingUnitDeliveries: return self.pending_orders(unit_type) + current_units def process(self, game: Game) -> None: + coalition = game.coalition_for(self.destination.captured) ground_unit_source = self.find_ground_unit_source(game) if ground_unit_source is None: game.message( f"{self.destination.name} lost its source for ground unit " "reinforcements. Refunding purchase price." ) - self.refund_ground_units(game) + self.refund_ground_units(coalition) bought_units: dict[UnitType[Any], int] = {} units_needing_transfer: dict[GroundUnitType, int] = {} sold_units: dict[UnitType[Any], int] = {} for unit_type, count in self.units.items(): - coalition = "Ally" if self.destination.captured else "Enemy" + allegiance = "Ally" if self.destination.captured else "Enemy" d: dict[Any, int] if ( isinstance(unit_type, GroundUnitType) @@ -98,11 +98,11 @@ class PendingUnitDeliveries: if count >= 0: d[unit_type] = count game.message( - f"{coalition} reinforcements: {unit_type} x {count} at {source}" + f"{allegiance} reinforcements: {unit_type} x {count} at {source}" ) else: sold_units[unit_type] = -count - game.message(f"{coalition} sold: {unit_type} x {-count} at {source}") + game.message(f"{allegiance} sold: {unit_type} x {-count} at {source}") self.units = defaultdict(int) self.destination.base.commission_units(bought_units) @@ -111,16 +111,19 @@ class PendingUnitDeliveries: if units_needing_transfer: if ground_unit_source is None: raise RuntimeError( - f"ground unit source could not be found for {self.destination} but still tried to " - f"transfer units to there" + f"Ground unit source could not be found for {self.destination} but " + "still tried to transfer units to there" ) ground_unit_source.base.commission_units(units_needing_transfer) - self.create_transfer(game, ground_unit_source, units_needing_transfer) + self.create_transfer(coalition, ground_unit_source, units_needing_transfer) def create_transfer( - self, game: Game, source: ControlPoint, units: dict[GroundUnitType, int] + self, + coalition: Coalition, + source: ControlPoint, + units: dict[GroundUnitType, int], ) -> None: - game.transfers.new_transfer(TransferOrder(source, self.destination, units)) + coalition.transfers.new_transfer(TransferOrder(source, self.destination, units)) def find_ground_unit_source(self, game: Game) -> Optional[ControlPoint]: # This is running *after* the turn counter has been incremented, so this is the diff --git a/gen/aircraft.py b/gen/aircraft.py index c0caa0a0..392cd70c 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -22,7 +22,6 @@ from dcs.planes import ( C_101EB, F_14B, JF_17, - PlaneType, Su_33, Tu_22M3, ) @@ -262,8 +261,8 @@ class AircraftConflictGenerator: @cached_property def use_client(self) -> bool: """True if Client should be used instead of Player.""" - blue_clients = self.client_slots_in_ato(self.game.blue_ato) - red_clients = self.client_slots_in_ato(self.game.red_ato) + blue_clients = self.client_slots_in_ato(self.game.blue.ato) + red_clients = self.client_slots_in_ato(self.game.red.ato) return blue_clients + red_clients > 1 @staticmethod @@ -601,12 +600,11 @@ class AircraftConflictGenerator: if not isinstance(control_point, Airfield): continue + faction = self.game.coalition_for(control_point.captured).faction if control_point.captured: country = player_country - faction = self.game.player_faction else: country = enemy_country - faction = self.game.enemy_faction for aircraft, available in inventory.all_aircraft: try: @@ -699,11 +697,7 @@ class AircraftConflictGenerator: if flight.from_cp.cptype != ControlPointType.AIRBASE: return - if flight.from_cp.captured: - coalition = self.game.get_player_coalition_id() - else: - coalition = self.game.get_enemy_coalition_id() - + coalition = self.game.coalition_for(flight.departure.captured).coalition_id trigger.add_condition(CoalitionHasAirdrome(coalition, flight.from_cp.id)) def generate_planned_flight( diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py index 37fc30bb..409a0959 100644 --- a/gen/airsupportgen.py +++ b/gen/airsupportgen.py @@ -105,6 +105,8 @@ class AirSupportConflictGenerator: else self.conflict.red_cp ) + country = self.mission.country(self.game.blue.country_name) + if not self.game.settings.disable_legacy_tanker: fallback_tanker_number = 0 @@ -130,10 +132,8 @@ class AirSupportConflictGenerator: tanker_heading, TANKER_DISTANCE ) tanker_group = self.mission.refuel_flight( - country=self.mission.country(self.game.player_country), - name=namegen.next_tanker_name( - self.mission.country(self.game.player_country), tanker_unit_type - ), + country=country, + name=namegen.next_tanker_name(country, tanker_unit_type), airport=None, plane_type=unit_type, position=tanker_position, @@ -211,10 +211,8 @@ class AirSupportConflictGenerator: return awacs_flight = self.mission.awacs_flight( - country=self.mission.country(self.game.player_country), - name=namegen.next_awacs_name( - self.mission.country(self.game.player_country) - ), + country=country, + name=namegen.next_awacs_name(country), plane_type=unit_type, altitude=AWACS_ALT, airport=None, diff --git a/gen/armor.py b/gen/armor.py index 6db4f632..7e92169b 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -144,16 +144,16 @@ class GroundConflictGenerator: ) # Add JTAC - if self.game.player_faction.has_jtac: + if self.game.blue.faction.has_jtac: n = "JTAC" + str(self.conflict.blue_cp.id) + str(self.conflict.red_cp.id) code = 1688 - len(self.jtacs) - utype = self.game.player_faction.jtac_unit + utype = self.game.blue.faction.jtac_unit if utype is None: utype = AircraftType.named("MQ-9 Reaper") jtac = self.mission.flight_group( - country=self.mission.country(self.game.player_country), + country=self.mission.country(self.game.blue.country_name), name=n, aircraft_type=utype.dcs_unit_type, position=position[0], @@ -715,7 +715,7 @@ class GroundConflictGenerator: if is_player else int(heading_sum(heading, 90)) ) - country = self.game.player_country if is_player else self.game.enemy_country + country = self.game.coalition_for(is_player).country_name for group in groups: if group.role == CombatGroupRole.ARTILLERY: distance_from_frontline = ( diff --git a/gen/cargoshipgen.py b/gen/cargoshipgen.py index 9de370b9..ec7e6577 100644 --- a/gen/cargoshipgen.py +++ b/gen/cargoshipgen.py @@ -24,12 +24,13 @@ class CargoShipGenerator: def generate(self) -> None: # Reset the count to make generation deterministic. - for ship in self.game.transfers.cargo_ships: - self.generate_cargo_ship(ship) + for coalition in self.game.coalitions: + for ship in coalition.transfers.cargo_ships: + self.generate_cargo_ship(ship) def generate_cargo_ship(self, ship: CargoShip) -> ShipGroup: country = self.mission.country( - self.game.player_country if ship.player_owned else self.game.enemy_country + self.game.coalition_for(ship.player_owned).country_name ) waypoints = ship.route group = self.mission.ship_group( diff --git a/gen/convoygen.py b/gen/convoygen.py index 303c286f..b695d144 100644 --- a/gen/convoygen.py +++ b/gen/convoygen.py @@ -27,8 +27,9 @@ class ConvoyGenerator: def generate(self) -> None: # Reset the count to make generation deterministic. - for convoy in self.game.transfers.convoys: - self.generate_convoy(convoy) + for coalition in self.game.coalitions: + for convoy in coalition.transfers.convoys: + self.generate_convoy(convoy) def generate_convoy(self, convoy: Convoy) -> VehicleGroup: group = self._create_mixed_unit_group( @@ -53,9 +54,7 @@ class ConvoyGenerator: units: dict[GroundUnitType, int], for_player: bool, ) -> VehicleGroup: - country = self.mission.country( - self.game.player_country if for_player else self.game.enemy_country - ) + country = self.mission.country(self.game.coalition_for(for_player).country_name) unit_types = list(units.items()) main_unit_type, main_unit_count = unit_types[0] diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index cbe234b1..7c9e3e22 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -228,7 +228,7 @@ class CoalitionMissionPlanner: self.game = game self.is_player = is_player self.objective_finder = ObjectiveFinder(self.game, self.is_player) - self.ato = self.game.blue_ato if is_player else self.game.red_ato + self.ato = self.game.coalition_for(is_player).ato self.threat_zones = self.game.threat_zone_for(not self.is_player) self.procurement_requests = self.game.procurement_requests_for(self.is_player) self.faction: Faction = self.game.faction_for(self.is_player) diff --git a/gen/forcedoptionsgen.py b/gen/forcedoptionsgen.py index ea1c854b..e4025d48 100644 --- a/gen/forcedoptionsgen.py +++ b/gen/forcedoptionsgen.py @@ -38,8 +38,8 @@ class ForcedOptionsGenerator: self.mission.forced_options.labels = ForcedOptions.Labels.None_ def _set_unrestricted_satnav(self) -> None: - blue = self.game.player_faction - red = self.game.enemy_faction + blue = self.game.blue.faction + red = self.game.red.faction if blue.unrestricted_satnav or red.unrestricted_satnav: self.mission.forced_options.unrestricted_satnav = True diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index ad6d0262..c7b7ca53 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -25,7 +25,7 @@ from typing import ( from dcs import Mission, Point, unitgroup from dcs.action import SceneryDestructionZone from dcs.country import Country -from dcs.point import StaticPoint, MovingPoint +from dcs.point import StaticPoint from dcs.statics import Fortification, fortification_map, warehouse_map from dcs.task import ( ActivateBeaconCommand, @@ -36,8 +36,8 @@ from dcs.task import ( ) from dcs.triggers import TriggerStart, TriggerZone from dcs.unit import Ship, Unit, Vehicle, SingleHeliPad -from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup -from dcs.unittype import StaticType, UnitType, ShipType, VehicleType +from dcs.unitgroup import ShipGroup, StaticGroup, VehicleGroup +from dcs.unittype import StaticType, ShipType, VehicleType from dcs.vehicles import vehicle_map from game import db @@ -587,13 +587,7 @@ class HelipadGenerator: self.tacan_registry = tacan_registry def generate(self) -> None: - - if self.cp.captured: - country_name = self.game.player_country - else: - country_name = self.game.enemy_country - country = self.m.country(country_name) - + country = self.m.country(self.game.coalition_for(self.cp.captured).country_name) for i, helipad in enumerate(self.cp.helipads): name = self.cp.name + "_helipad_" + str(i) logging.info("Generating helipad : " + name) @@ -636,12 +630,7 @@ class GroundObjectsGenerator: def generate(self) -> None: for cp in self.game.theater.controlpoints: - if cp.captured: - country_name = self.game.player_country - else: - country_name = self.game.enemy_country - country = self.m.country(country_name) - + country = self.m.country(self.game.coalition_for(cp.captured).country_name) HelipadGenerator( self.m, cp, self.game, self.radio_registry, self.tacan_registry ).generate() diff --git a/gen/visualgen.py b/gen/visualgen.py index 5d8ffead..83be4859 100644 --- a/gen/visualgen.py +++ b/gen/visualgen.py @@ -97,7 +97,7 @@ class VisualGenerator: break self.mission.static_group( - self.mission.country(self.game.enemy_country), + self.mission.country(self.game.red.country_name), "", _type=v, position=pos, diff --git a/qt_ui/models.py b/qt_ui/models.py index 4e5f46b3..ee842de5 100644 --- a/qt_ui/models.py +++ b/qt_ui/models.py @@ -12,11 +12,10 @@ from PySide2.QtCore import ( ) from PySide2.QtGui import QIcon -from game import db from game.game import Game from game.squadrons import Squadron, Pilot from game.theater.missiontarget import MissionTarget -from game.transfers import TransferOrder +from game.transfers import TransferOrder, PendingTransfers from gen.ato import AirTaskingOrder, Package from gen.flights.flight import Flight, FlightType from gen.flights.traveltime import TotEstimator @@ -281,9 +280,9 @@ class AtoModel(QAbstractListModel): self.package_models.clear() if self.game is not None: if player: - self.ato = self.game.blue_ato + self.ato = self.game.blue.ato else: - self.ato = self.game.red_ato + self.ato = self.game.red.ato else: self.ato = AirTaskingOrder() self.endResetModel() @@ -316,8 +315,12 @@ class TransferModel(QAbstractListModel): super().__init__() self.game_model = game_model + @property + def transfers(self) -> PendingTransfers: + return self.game_model.game.coalition_for(player=True).transfers + def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: - return self.game_model.game.transfers.pending_transfer_count + return self.transfers.pending_transfer_count def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any: if not index.isValid(): @@ -345,7 +348,7 @@ class TransferModel(QAbstractListModel): """Updates the game with the new unit transfer.""" self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) # TODO: Needs to regenerate base inventory tab. - self.game_model.game.transfers.new_transfer(transfer) + self.transfers.new_transfer(transfer) self.endInsertRows() def cancel_transfer_at_index(self, index: QModelIndex) -> None: @@ -354,15 +357,15 @@ class TransferModel(QAbstractListModel): def cancel_transfer(self, transfer: TransferOrder) -> None: """Cancels the planned unit transfer at the given index.""" - index = self.game_model.game.transfers.index_of_transfer(transfer) + index = self.transfers.index_of_transfer(transfer) self.beginRemoveRows(QModelIndex(), index, index) # TODO: Needs to regenerate base inventory tab. - self.game_model.game.transfers.cancel_transfer(transfer) + self.transfers.cancel_transfer(transfer) self.endRemoveRows() def transfer_at_index(self, index: QModelIndex) -> TransferOrder: """Returns the transfer located at the given index.""" - return self.game_model.game.transfers.transfer_at_index(index.row()) + return self.transfers.transfer_at_index(index.row()) class AirWingModel(QAbstractListModel): @@ -488,8 +491,8 @@ class GameModel: self.ato_model = AtoModel(self, AirTaskingOrder()) self.red_ato_model = AtoModel(self, AirTaskingOrder()) else: - self.ato_model = AtoModel(self, self.game.blue_ato) - self.red_ato_model = AtoModel(self, self.game.red_ato) + self.ato_model = AtoModel(self, self.game.blue.ato) + self.red_ato_model = AtoModel(self, self.game.red.ato) def ato_model_for(self, player: bool) -> AtoModel: if player: diff --git a/qt_ui/widgets/QFactionsInfos.py b/qt_ui/widgets/QFactionsInfos.py index 935f4af9..c0ca25cd 100644 --- a/qt_ui/widgets/QFactionsInfos.py +++ b/qt_ui/widgets/QFactionsInfos.py @@ -24,8 +24,8 @@ class QFactionsInfos(QGroupBox): def setGame(self, game: Game): if game is not None: - self.player_name.setText(game.player_faction.name) - self.enemy_name.setText(game.enemy_faction.name) + self.player_name.setText(game.blue.faction.name) + self.enemy_name.setText(game.red.faction.name) else: self.player_name.setText("") self.enemy_name.setText("") diff --git a/qt_ui/widgets/QTopPanel.py b/qt_ui/widgets/QTopPanel.py index 5f295a6a..d94f2911 100644 --- a/qt_ui/widgets/QTopPanel.py +++ b/qt_ui/widgets/QTopPanel.py @@ -168,7 +168,7 @@ class QTopPanel(QFrame): package.time_over_target = estimator.earliest_tot() def ato_has_clients(self) -> bool: - for package in self.game.blue_ato.packages: + for package in self.game.blue.ato.packages: for flight in package.flights: if flight.client_count > 0: return True @@ -236,7 +236,7 @@ class QTopPanel(QFrame): def check_no_missing_pilots(self) -> bool: missing_pilots = [] - for package in self.game.blue_ato.packages: + for package in self.game.blue.ato.packages: for flight in package.flights: if flight.missing_pilots > 0: missing_pilots.append((package, flight)) @@ -282,8 +282,8 @@ class QTopPanel(QFrame): closest_cps[0], closest_cps[1], self.game.theater.controlpoints[0].position, - self.game.player_faction.name, - self.game.enemy_faction.name, + self.game.blue.faction.name, + self.game.red.faction.name, ) unit_map = self.game.initiate_event(game_event) diff --git a/qt_ui/widgets/combos/QAircraftTypeSelector.py b/qt_ui/widgets/combos/QAircraftTypeSelector.py index 80dfa5b0..12f913db 100644 --- a/qt_ui/widgets/combos/QAircraftTypeSelector.py +++ b/qt_ui/widgets/combos/QAircraftTypeSelector.py @@ -4,7 +4,6 @@ from typing import Iterable, Type from PySide2.QtWidgets import QComboBox from dcs.unittype import FlyingType -from game import db from gen.flights.ai_flight_planner_db import aircraft_for_task from gen.flights.flight import FlightType @@ -13,16 +12,12 @@ class QAircraftTypeSelector(QComboBox): """Combo box for selecting among the given aircraft types.""" def __init__( - self, - aircraft_types: Iterable[Type[FlyingType]], - country: str, - mission_type: FlightType, + self, aircraft_types: Iterable[Type[FlyingType]], mission_type: FlightType ) -> None: super().__init__() self.model().sort(0) self.setSizeAdjustPolicy(self.AdjustToContents) - self.country = country self.update_items(mission_type, aircraft_types) def update_items(self, mission_type: FlightType, aircraft_types): diff --git a/qt_ui/widgets/map/mapmodel.py b/qt_ui/widgets/map/mapmodel.py index 3d56b5d8..c6c5e70b 100644 --- a/qt_ui/widgets/map/mapmodel.py +++ b/qt_ui/widgets/map/mapmodel.py @@ -336,8 +336,12 @@ class SupplyRouteJs(QObject): def find_transports(self) -> List[MultiGroupTransport]: if self.sea_route: - return self.find_in_transport_map(self.game.transfers.cargo_ships) - return self.find_in_transport_map(self.game.transfers.convoys) + return self.find_in_transport_map( + self.game.blue.transfers.cargo_ships + ) + self.find_in_transport_map(self.game.red.transfers.cargo_ships) + return self.find_in_transport_map( + self.game.blue.transfers.convoys + ) + self.find_in_transport_map(self.game.red.transfers.convoys) @Property(list, notify=activeTransportsChanged) def activeTransports(self) -> List[str]: @@ -672,8 +676,8 @@ class NavMeshJs(QObject): @classmethod def from_game(cls, game: Game) -> NavMeshJs: return NavMeshJs( - cls.to_polys(game.blue_navmesh, game.theater), - cls.to_polys(game.red_navmesh, game.theater), + cls.to_polys(game.blue.nav_mesh, game.theater), + cls.to_polys(game.red.nav_mesh, game.theater), ) @@ -870,8 +874,8 @@ class MapModel(QObject): def reset_atos(self) -> None: self._flights = self._flights_in_ato( - self.game.blue_ato, blue=True - ) + self._flights_in_ato(self.game.red_ato, blue=False) + self.game.blue.ato, blue=True + ) + self._flights_in_ato(self.game.red.ato, blue=False) self.flightsChanged.emit() @Property(list, notify=flightsChanged) diff --git a/qt_ui/windows/AirWingDialog.py b/qt_ui/windows/AirWingDialog.py index ac666e0e..df0cf81c 100644 --- a/qt_ui/windows/AirWingDialog.py +++ b/qt_ui/windows/AirWingDialog.py @@ -3,12 +3,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import Optional, Iterator -from PySide2.QtCore import ( - QItemSelectionModel, - QModelIndex, - Qt, - QSize, -) +from PySide2.QtCore import QItemSelectionModel, QModelIndex, QSize from PySide2.QtWidgets import ( QAbstractItemView, QCheckBox, @@ -183,7 +178,7 @@ class AirInventoryView(QWidget): self.table.setSortingEnabled(True) def iter_allocated_aircraft(self) -> Iterator[AircraftInventoryData]: - for package in self.game_model.game.blue_ato.packages: + for package in self.game_model.game.blue.ato.packages: for flight in package.flights: yield from AircraftInventoryData.from_flight(flight) diff --git a/qt_ui/windows/basemenu/DepartingConvoysMenu.py b/qt_ui/windows/basemenu/DepartingConvoysMenu.py index d858539e..c334f0bb 100644 --- a/qt_ui/windows/basemenu/DepartingConvoysMenu.py +++ b/qt_ui/windows/basemenu/DepartingConvoysMenu.py @@ -73,11 +73,15 @@ class DepartingConvoysList(QFrame): task_box_layout = QGridLayout() scroll_content.setLayout(task_box_layout) - for convoy in game_model.game.transfers.convoys.departing_from(cp): + for convoy in game_model.game.coalition_for( + cp.captured + ).transfers.convoys.departing_from(cp): group_info = DepartingConvoyInfo(convoy) task_box_layout.addWidget(group_info) - for cargo_ship in game_model.game.transfers.cargo_ships.departing_from(cp): + for cargo_ship in game_model.game.coalition_for( + cp.captured + ).transfers.cargo_ships.departing_from(cp): group_info = DepartingConvoyInfo(cargo_ship) task_box_layout.addWidget(group_info) diff --git a/qt_ui/windows/basemenu/QBaseMenu2.py b/qt_ui/windows/basemenu/QBaseMenu2.py index 5361350e..20dbf8f1 100644 --- a/qt_ui/windows/basemenu/QBaseMenu2.py +++ b/qt_ui/windows/basemenu/QBaseMenu2.py @@ -195,7 +195,9 @@ class QBaseMenu2(QDialog): ground_unit_limit = self.cp.frontline_unit_count_limit deployable_unit_info = "" - allocated = self.cp.allocated_ground_units(self.game_model.game.transfers) + allocated = self.cp.allocated_ground_units( + self.game_model.game.coalition_for(self.cp.captured).transfers + ) unit_overage = max( allocated.total_present - self.cp.frontline_unit_count_limit, 0 ) diff --git a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py index c5edcdbc..e435fd75 100644 --- a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py @@ -45,7 +45,7 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): row = 0 unit_types: Set[AircraftType] = set() - for unit_type in self.game_model.game.player_faction.aircrafts: + for unit_type in self.game_model.game.blue.faction.aircrafts: if self.cp.is_carrier and not unit_type.carrier_capable: continue if self.cp.is_lha and not unit_type.lha_capable: diff --git a/qt_ui/windows/groundobject/QGroundObjectMenu.py b/qt_ui/windows/groundobject/QGroundObjectMenu.py index 46b2cb53..0e629db8 100644 --- a/qt_ui/windows/groundobject/QGroundObjectMenu.py +++ b/qt_ui/windows/groundobject/QGroundObjectMenu.py @@ -1,7 +1,6 @@ import logging from typing import List, Optional -from PySide2 import QtCore from PySide2.QtGui import Qt from PySide2.QtWidgets import ( QComboBox, @@ -307,7 +306,7 @@ class QBuyGroupForGroundObjectDialog(QDialog): self.buySamBox = QGroupBox("Buy SAM site :") self.buyArmorBox = QGroupBox("Buy defensive position :") - faction = self.game.player_faction + faction = self.game.blue.faction # Sams diff --git a/qt_ui/windows/mission/flight/QFlightCreator.py b/qt_ui/windows/mission/flight/QFlightCreator.py index 7f5c6cc4..3c0a1e74 100644 --- a/qt_ui/windows/mission/flight/QFlightCreator.py +++ b/qt_ui/windows/mission/flight/QFlightCreator.py @@ -38,7 +38,7 @@ class QFlightCreator(QDialog): self.game = game self.package = package self.custom_name_text = None - self.country = self.game.player_country + self.country = self.game.blue.country_name self.setWindowTitle("Create flight") self.setWindowIcon(EVENT_ICONS["strike"]) @@ -52,7 +52,6 @@ class QFlightCreator(QDialog): self.aircraft_selector = QAircraftTypeSelector( self.game.aircraft_inventory.available_types_for_player, - self.game.player_country, self.task_selector.currentData(), ) self.aircraft_selector.setCurrentIndex(0)