diff --git a/game/debriefing.py b/game/debriefing.py index 5886270e..b33b24b6 100644 --- a/game/debriefing.py +++ b/game/debriefing.py @@ -87,8 +87,8 @@ class Debriefing: for i, ground_object in enumerate(cp.ground_objects): logging.info(unit) - logging.info(ground_object.string_identifier) - if ground_object.matches_string_identifier(unit): + logging.info(ground_object.group_name) + if ground_object.is_same_group(unit): unit = DebriefingDeadUnitInfo(country, player_unit, ground_object.dcs_identifier) self.dead_buildings.append(unit) elif ground_object.dcs_identifier in ["AA", "CARRIER", "LHA"]: diff --git a/game/event/event.py b/game/event/event.py index a45b64eb..5eda578b 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -145,8 +145,8 @@ class Event: if ground_object.is_dead: continue - if ground_object.matches_string_identifier(destroyed_ground_unit_name): - logging.info("cp {} killing ground object {}".format(cp, ground_object.string_identifier)) + if ground_object.is_same_group(destroyed_ground_unit_name): + logging.info("cp {} killing ground object {}".format(cp, ground_object.group_name)) cp.ground_objects[i].is_dead = True info = Information("Building destroyed", diff --git a/gen/aircraft.py b/gen/aircraft.py index f2269012..fccfdacb 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -1298,7 +1298,7 @@ class DeadIngressBuilder(PydcsWaypointBuilder): target_group = self.package.target if isinstance(target_group, TheaterGroundObject): - tgroup = self.mission.find_group(target_group.group_identifier, search="match") # Match search is used due to TheaterGroundObject.name not matching + tgroup = self.mission.find_group(target_group.group_name, search="match") # Match search is used due to TheaterGroundObject.name not matching if tgroup is not None: # the Mission group name because of SkyNet prefixes. task = AttackGroup(tgroup.id) task.params["expend"] = "All" @@ -1309,7 +1309,7 @@ class DeadIngressBuilder(PydcsWaypointBuilder): task.params["groupAttack"] = True waypoint.tasks.append(task) else: - logging.error(f"Could not find group for DEAD mission {target_group.group_identifier}") + logging.error(f"Could not find group for DEAD mission {target_group.group_name}") for i, t in enumerate(self.waypoint.targets): if self.group.units[0].unit_type == JF_17 and i < 4: @@ -1327,7 +1327,7 @@ class SeadIngressBuilder(PydcsWaypointBuilder): target_group = self.package.target if isinstance(target_group, TheaterGroundObject): - tgroup = self.mission.find_group(target_group.group_identifier, search="match") # Match search is used due to TheaterGroundObject.name not matching + tgroup = self.mission.find_group(target_group.group_name, search="match") # Match search is used due to TheaterGroundObject.name not matching if tgroup is not None: # the Mission group name because of SkyNet prefixes. waypoint.add_task(EngageTargetsInZone( position=tgroup.position, @@ -1337,7 +1337,7 @@ class SeadIngressBuilder(PydcsWaypointBuilder): ]) ) else: - logging.error(f"Could not find group for DEAD mission {target_group.group_identifier}") + logging.error(f"Could not find group for DEAD mission {target_group.group_name}") for i, t in enumerate(self.waypoint.targets): if self.group.units[0].unit_type == JF_17 and i < 4: diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index e0e5c1ee..1989452e 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -135,7 +135,7 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator): if not self.ground_object.is_dead: self.m.vehicle_group( country=self.country, - name=self.ground_object.string_identifier, + name=self.ground_object.group_name, _type=unit_type, position=self.ground_object.position, heading=self.ground_object.heading, @@ -144,7 +144,7 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator): def generate_static(self, static_type: StaticType) -> None: self.m.static_group( country=self.country, - name=self.ground_object.string_identifier, + name=self.ground_object.group_name, _type=static_type, position=self.ground_object.position, heading=self.ground_object.heading, diff --git a/gen/sam/genericsam_group_generator.py b/gen/sam/genericsam_group_generator.py index 00d5ed6e..8a35e51b 100644 --- a/gen/sam/genericsam_group_generator.py +++ b/gen/sam/genericsam_group_generator.py @@ -1,19 +1,15 @@ -import random +from abc import ABC -from dcs.vehicles import AirDefence -from game import db +from game import Game from gen.sam.group_generator import GroupGenerator +from theater.theatergroundobject import SamGroundObject -class GenericSamGroupGenerator(GroupGenerator): +class GenericSamGroupGenerator(GroupGenerator, ABC): """ This is the base for all SAM group generators """ - @property - def groupNamePrefix(self) -> str: - # prefix the SAM site for use with the Skynet IADS plugin - if self.faction == self.game.player_name: # this is the player faction - return "BLUE SAM " - else: - return "RED SAM " + def __init__(self, game: Game, ground_object: SamGroundObject) -> None: + ground_object.skynet_capable = True + super().__init__(game, ground_object) diff --git a/gen/sam/group_generator.py b/gen/sam/group_generator.py index f35f1a0e..94738eef 100644 --- a/gen/sam/group_generator.py +++ b/gen/sam/group_generator.py @@ -22,31 +22,26 @@ if TYPE_CHECKING: # types rather than pydcs groups. class GroupGenerator: - def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Optional[Faction] = None): # faction is not mandatory because some subclasses do not use it + def __init__(self, game: Game, ground_object: TheaterGroundObject) -> None: self.game = game self.go = ground_object self.position = ground_object.position self.heading = random.randint(0, 359) - self.faction = faction - self.vg = unitgroup.VehicleGroup(self.game.next_group_id(), self.groupNamePrefix + self.go.group_identifier) + self.vg = unitgroup.VehicleGroup(self.game.next_group_id(), + self.go.group_name) wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0) wp.ETA_locked = True - @property - def groupNamePrefix(self) -> str: - return "" - def generate(self): raise NotImplementedError def get_generated_group(self) -> unitgroup.VehicleGroup: return self.vg - def add_unit(self, unit_type: VehicleType, name: str, pos_x: float, pos_y: float, heading: int): - nn = "cgroup|" + str(self.go.cp_id) + '|' + str(self.go.group_id) + '|' + str(self.go.group_identifier) + "|" + name - + def add_unit(self, unit_type: VehicleType, name: str, pos_x: float, + pos_y: float, heading: int) -> Vehicle: unit = Vehicle(self.game.next_unit_id(), - nn, unit_type.id) + f"{self.go.group_name}|{name}", unit_type.id) unit.position.x = pos_x unit.position.y = pos_y unit.heading = heading @@ -88,6 +83,7 @@ class GroupGenerator: current_offset += outer_offset return positions + class ShipGroupGenerator(GroupGenerator): """Abstract class for other ship generator classes""" def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction): @@ -96,15 +92,14 @@ class ShipGroupGenerator(GroupGenerator): self.position = ground_object.position self.heading = random.randint(0, 359) self.faction = faction - self.vg = unitgroup.ShipGroup(self.game.next_group_id(), self.groupNamePrefix + self.go.group_identifier) + self.vg = unitgroup.ShipGroup(self.game.next_group_id(), + self.go.group_name) wp = self.vg.add_waypoint(self.position, 0) wp.ETA_locked = True - def add_unit(self, unit_type, name, pos_x, pos_y, heading): - nn = "cgroup|" + str(self.go.cp_id) + '|' + str(self.go.group_id) + '|' + str(self.go.group_identifier) + "|" + name - + def add_unit(self, unit_type, name, pos_x, pos_y, heading) -> Ship: unit = Ship(self.game.next_unit_id(), - nn, unit_type) + f"{self.go.group_name}|{name}", unit_type) unit.position.x = pos_x unit.position.y = pos_y unit.heading = heading diff --git a/gen/sam/sam_group_generator.py b/gen/sam/sam_group_generator.py index 5bafd6f5..2a991f04 100644 --- a/gen/sam/sam_group_generator.py +++ b/gen/sam/sam_group_generator.py @@ -38,6 +38,7 @@ from gen.sam.sam_zu23_ural import ZU23UralGenerator from gen.sam.sam_zu23_ural_insurgent import ZU23UralInsurgentGenerator from gen.sam.freya_ewr import FreyaGenerator from theater import TheaterGroundObject +from theater.theatergroundobject import SamGroundObject SAM_MAP = { "HawkGenerator": HawkGenerator, @@ -129,13 +130,13 @@ def generate_anti_air_group(game: Game, ground_object: TheaterGroundObject, possible_sams_generators = get_faction_possible_sams_generator(faction) if len(possible_sams_generators) > 0: sam_generator_class = random.choice(possible_sams_generators) - generator = sam_generator_class(game, ground_object, db.FACTIONS[faction]) + generator = sam_generator_class(game, ground_object) generator.generate() return generator.get_generated_group() return None -def generate_shorad_group(game: Game, ground_object: TheaterGroundObject, +def generate_shorad_group(game: Game, ground_object: SamGroundObject, faction_name: str) -> Optional[VehicleGroup]: faction = db.FACTIONS[faction_name] diff --git a/qt_ui/widgets/combos/QFlightTypeComboBox.py b/qt_ui/widgets/combos/QFlightTypeComboBox.py index 429ff902..d1a27382 100644 --- a/qt_ui/widgets/combos/QFlightTypeComboBox.py +++ b/qt_ui/widgets/combos/QFlightTypeComboBox.py @@ -95,7 +95,7 @@ class QFlightTypeComboBox(QComboBox): yield from self.ENEMY_AIRBASE_MISSIONS elif isinstance(self.target, TheaterGroundObject): # TODO: Filter more based on the category. - friendly = self.target.parent_control_point(self.theater).captured + friendly = self.target.control_point.captured if friendly: yield from self.FRIENDLY_GROUND_OBJECT_MISSIONS else: diff --git a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py index aac3b3c4..72ece41e 100644 --- a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py +++ b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py @@ -1,7 +1,7 @@ from PySide2.QtGui import QStandardItem, QStandardItemModel from game import Game -from gen import Conflict, FlightWaypointType +from gen import BuildingGroundObject, Conflict, FlightWaypointType from gen.flights.flight import FlightWaypoint from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox from theater import ControlPointType @@ -71,7 +71,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): for cp in self.game.theater.controlpoints: if (self.include_enemy and not cp.captured) or (self.include_friendly and cp.captured): for ground_object in cp.ground_objects: - if not ground_object.is_dead and not ground_object.dcs_identifier == "AA": + if not ground_object.is_dead and not isinstance(ground_object, BuildingGroundObject): wpt = FlightWaypoint( FlightWaypointType.CUSTOM, ground_object.position.x, diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 9c5b414c..0a404555 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -121,8 +121,8 @@ class QLiberationMap(QGraphicsView): def setGame(self, game: Optional[Game]): self.game = game - logging.debug("Reloading Map Canvas") if self.game is not None: + logging.debug("Reloading Map Canvas") self.reload_scene() """ diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index 45520c5c..67dba6f8 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -1,4 +1,5 @@ import logging +import traceback import webbrowser from typing import Optional @@ -16,7 +17,7 @@ from PySide2.QtWidgets import ( ) import qt_ui.uiconstants as CONST -from game import Game, persistency, VERSION +from game import Game, VERSION, persistency from qt_ui.dialogs import Dialog from qt_ui.displayoptions import DisplayGroup, DisplayOptions, DisplayRule from qt_ui.models import GameModel @@ -40,10 +41,9 @@ class QLiberationWindow(QMainWindow): self.game: Optional[Game] = None self.game_model = GameModel() Dialog.set_game(self.game_model) - self.ato_panel = None - self.info_panel = None - self.liberation_map = None - self.setGame(persistency.restore_game()) + self.ato_panel = QAirTaskingOrderPanel(self.game_model) + self.info_panel = QInfoPanel(self.game) + self.liberation_map = QLiberationMap(self.game_model) self.setGeometry(300, 100, 270, 100) self.setWindowTitle(f"DCS Liberation - v{VERSION}") @@ -55,17 +55,14 @@ class QLiberationWindow(QMainWindow): self.initMenuBar() self.initToolbar() self.connectSignals() - self.onGameGenerated(self.game) screen = QDesktopWidget().screenGeometry() self.setGeometry(0, 0, screen.width(), screen.height()) self.setWindowState(Qt.WindowMaximized) - def initUi(self): - self.ato_panel = QAirTaskingOrderPanel(self.game_model) - self.liberation_map = QLiberationMap(self.game_model) - self.info_panel = QInfoPanel(self.game) + self.onGameGenerated(persistency.restore_game()) + def initUi(self): hbox = QSplitter(Qt.Horizontal) vbox = QSplitter(Qt.Vertical) hbox.addWidget(self.ato_panel) @@ -193,8 +190,7 @@ class QLiberationWindow(QMainWindow): filter="*.liberation") if file is not None: game = persistency.load_game(file[0]) - self.setGame(game) - GameUpdateSignal.get_instance().updateGame(self.game) + GameUpdateSignal.get_instance().updateGame(game) def saveGame(self): logging.info("Saving game") @@ -217,14 +213,27 @@ class QLiberationWindow(QMainWindow): GameUpdateSignal.get_instance().updateGame(self.game) def setGame(self, game: Optional[Game]): - if game is not None: - game.on_load() - self.game = game - if self.info_panel is not None: - self.info_panel.setGame(game) - self.game_model.set(self.game) - if self.liberation_map is not None: - self.liberation_map.setGame(game) + try: + if game is not None: + game.on_load() + self.game = game + if self.info_panel is not None: + self.info_panel.setGame(game) + self.game_model.set(self.game) + if self.liberation_map is not None: + self.liberation_map.setGame(game) + except AttributeError: + logging.exception("Incompatible save game") + QMessageBox.critical( + self, + "Could not load save game", + "The save game you have loaded is incompatible with this " + "version of DCS Liberation.\n" + "\n" + f"{traceback.format_exc()}", + QMessageBox.Ok + ) + GameUpdateSignal.get_instance().updateGame(None) def showAboutDialog(self): text = "

DCS Liberation " + VERSION + "

" + \ diff --git a/resources/plugins/skynetiads/skynetiads-config.lua b/resources/plugins/skynetiads/skynetiads-config.lua index ea6cb4a5..df12d512 100644 --- a/resources/plugins/skynetiads/skynetiads-config.lua +++ b/resources/plugins/skynetiads/skynetiads-config.lua @@ -72,10 +72,10 @@ if dcsLiberation and SkynetIADS then end --add EW units to the IADS: - iads:addEarlyWarningRadarsByPrefix(coalitionPrefix .. " EW") + iads:addEarlyWarningRadarsByPrefix(coalitionPrefix .. "|EWR|") --add SAM groups to the IADS: - iads:addSAMSitesByPrefix(coalitionPrefix .. " SAM") + iads:addSAMSitesByPrefix(coalitionPrefix .. "|SAM|") -- specific configurations, for each SAM type if actAsEwr then diff --git a/resources/stylesheets/style-dcs.css b/resources/stylesheets/style-dcs.css index bd866559..dab1b553 100644 --- a/resources/stylesheets/style-dcs.css +++ b/resources/stylesheets/style-dcs.css @@ -19,6 +19,14 @@ grey text -------------------- #B7C0C6 */ +/* + * Makes all message box text selectable. + * https://stackoverflow.com/a/32595502/632035 + */ +QMessageBox { + messagebox-text-interaction-flags: 5; +} + /*QMenuBar*/ QMenuBar { spacing: 2px; /* spacing between menu bar items */ diff --git a/resources/stylesheets/windows-style.css b/resources/stylesheets/windows-style.css index cbf5010f..5d6c99ea 100644 --- a/resources/stylesheets/windows-style.css +++ b/resources/stylesheets/windows-style.css @@ -1,3 +1,11 @@ /* windows basis styles */ + +/* + * Makes all message box text selectable. + * https://stackoverflow.com/a/32595502/632035 + */ +QMessageBox { + messagebox-text-interaction-flags: 5; +} \ No newline at end of file diff --git a/theater/controlpoint.py b/theater/controlpoint.py index 4aaedf21..d748d797 100644 --- a/theater/controlpoint.py +++ b/theater/controlpoint.py @@ -1,5 +1,6 @@ from __future__ import annotations +import itertools import re from typing import Dict, List, TYPE_CHECKING from enum import Enum @@ -17,7 +18,7 @@ from game import db from gen.ground_forces.combat_stance import CombatStance from .base import Base from .missiontarget import MissionTarget -from .theatergroundobject import TheaterGroundObject +from .theatergroundobject import SamGroundObject, TheaterGroundObject if TYPE_CHECKING: from game import Game @@ -50,7 +51,8 @@ class ControlPoint(MissionTarget): self.id = id self.full_name = name self.at = at - self.ground_objects: List[TheaterGroundObject] = [] + self.connected_objectives: List[TheaterGroundObject] = [] + self.base_defenses: List[SamGroundObject] = [] self.size = size self.importance = importance @@ -64,6 +66,11 @@ class ControlPoint(MissionTarget): self.stances: Dict[int, CombatStance] = {} self.airport = None + @property + def ground_objects(self) -> List[TheaterGroundObject]: + return list( + itertools.chain(self.connected_objectives, self.base_defenses)) + @classmethod def from_airport(cls, airport: Airport, radials: List[int], size: int, importance: float, has_frontline=True): assert airport @@ -225,9 +232,6 @@ class ControlPoint(MissionTarget): self.base.armor = {} # Handle cyclic dependency. - from .start_generator import generate_airbase_defense_group - for idx, ground_object in enumerate(self.ground_objects): - ground_object.groups = [] - if ground_object.airbase_group and faction_name != "": - generate_airbase_defense_group(idx, ground_object, - faction_name, game) + from .start_generator import BaseDefenseGenerator + self.base_defenses = [] + BaseDefenseGenerator(game, self, faction_name).generate() diff --git a/theater/start_generator.py b/theater/start_generator.py index 602470aa..84a28e4d 100644 --- a/theater/start_generator.py +++ b/theater/start_generator.py @@ -160,11 +160,9 @@ class GameGenerator: class ControlPointGroundObjectGenerator: - def __init__(self, game: Game, control_point: ControlPoint, - templates: GroundObjectTemplates) -> None: + def __init__(self, game: Game, control_point: ControlPoint) -> None: self.game = game self.control_point = control_point - self.templates = templates @property def faction_name(self) -> str: @@ -178,8 +176,7 @@ class ControlPointGroundObjectGenerator: return db.FACTIONS[self.faction_name] def generate(self) -> bool: - self.control_point.ground_objects = [] - self.generate_ground_points() + self.control_point.connected_objectives = [] if self.faction.navy_generators: # Even airbases can generate navies if they are close enough to the # water. This is not controlled by the control point definition, but @@ -187,85 +184,8 @@ class ControlPointGroundObjectGenerator: # for the ship. self.generate_navy() - if self.faction.missiles: - # TODO: Presumably only for airbases? - self.generate_missile_sites() - return True - def generate_ground_points(self) -> None: - """Generate ground objects and AA sites for the control point.""" - - if self.control_point.is_global: - return - - # TODO: Should probably perform this check later. - # Just because we don't have factories for the faction doesn't mean we - # shouldn't generate AA. - available_categories = self.faction.building_set - if not available_categories: - return - - # Always generate at least one AA point. - self.generate_aa_site() - - # And between 2 and 7 other objectives. - amount = random.randrange(2, 7) - for i in range(amount): - # 1 in 4 additional objectives are AA. - if random.randint(0, 3) == 0: - self.generate_aa_site() - else: - category = random.choice(available_categories) - self.generate_ground_point(category) - - def generate_ground_point(self, category: str) -> None: - obj_name = namegen.random_objective_name() - template = random.choice(list(self.templates[category].values())) - point = find_location(category != "oil", - self.control_point.position, - self.game.theater, 10000, 40000, - self.control_point.ground_objects) - - if point is None: - logging.error( - f"Could not find point for {obj_name} at {self.control_point}") - return - - object_id = 0 - group_id = self.game.next_group_id() - - # TODO: Create only one TGO per objective, each with multiple units. - for unit in template: - object_id += 1 - - template_point = Point(unit["offset"].x, unit["offset"].y) - g = BuildingGroundObject( - obj_name, category, group_id, object_id, point + template_point, - unit["heading"], self.control_point, unit["type"]) - - self.control_point.ground_objects.append(g) - - def generate_aa_site(self) -> None: - obj_name = namegen.random_objective_name() - position = find_location(True, self.control_point.position, - self.game.theater, 10000, 40000, - self.control_point.ground_objects) - - if position is None: - logging.error( - f"Could not find point for {obj_name} at {self.control_point}") - return - - group_id = self.game.next_group_id() - - g = SamGroundObject(namegen.random_objective_name(), group_id, - position, self.control_point, for_airbase=False) - group = generate_anti_air_group(self.game, g, self.faction_name) - if group is not None: - g.groups = [group] - self.control_point.ground_objects.append(g) - def generate_navy(self) -> None: skip_player_navy = self.game.settings.do_not_generate_player_navy if self.control_point.captured and skip_player_navy: @@ -295,30 +215,7 @@ class ControlPointGroundObjectGenerator: g.groups = [] if group is not None: g.groups.append(group) - self.control_point.ground_objects.append(g) - - def generate_missile_sites(self) -> None: - for i in range(self.faction.missiles_group_count): - self.generate_missile_site() - - def generate_missile_site(self) -> None: - point = find_location(True, self.control_point.position, - self.game.theater, 2500, 40000, [], False) - if point is None: - logging.info( - f"Could not find point for {self.control_point} missile site") - return - - group_id = self.game.next_group_id() - - g = MissileSiteGroundObject(namegen.random_objective_name(), group_id, - point, self.control_point) - group = generate_missile_group(self.game, g, self.faction_name) - g.groups = [] - if group is not None: - g.groups.append(group) - self.control_point.ground_objects.append(g) - return + self.control_point.connected_objectives.append(g) class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator): @@ -341,7 +238,7 @@ class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator): g.groups = [] if group is not None: g.groups.append(group) - self.control_point.ground_objects.append(g) + self.control_point.connected_objectives.append(g) self.control_point.name = random.choice(carrier_names) return True @@ -366,21 +263,23 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator): g.groups = [] if group is not None: g.groups.append(group) - self.control_point.ground_objects.append(g) + self.control_point.connected_objectives.append(g) self.control_point.name = random.choice(lha_names) return True -class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): - def generate(self) -> bool: - if not super().generate(): - return False +class BaseDefenseGenerator: + def __init__(self, game: Game, control_point: ControlPoint, + faction_name: str) -> None: + self.game = game + self.control_point = control_point + self.faction_name = faction_name + def generate(self) -> None: for i in range(random.randint(3, 6)): - self.generate_sam(i) - return True + self.generate_base_defense(i) - def generate_sam(self, index: int) -> None: + def generate_base_defense(self, index: int) -> None: position = find_location(True, self.control_point.position, self.game.theater, 400, 3200, [], True) @@ -403,8 +302,122 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): g = SamGroundObject(namegen.random_objective_name(), group_id, position, self.control_point, for_airbase=True) - generate_airbase_defense_group(index, g, self.faction_name, self.game) - self.control_point.ground_objects.append(g) + generate_airbase_defense_group(index, g, self.faction_name, + self.game) + self.control_point.base_defenses.append(g) + + +class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): + def __init__(self, game: Game, control_point: ControlPoint, + templates: GroundObjectTemplates) -> None: + super().__init__(game, control_point) + self.templates = templates + + def generate(self) -> bool: + if not super().generate(): + return False + + BaseDefenseGenerator(self.game, self.control_point, + self.faction_name).generate() + self.generate_ground_points() + + if self.faction.missiles: + self.generate_missile_sites() + + return True + + def generate_ground_points(self) -> None: + """Generate ground objects and AA sites for the control point.""" + if self.control_point.is_global: + return + + # Always generate at least one AA point. + self.generate_aa_site() + + # And between 2 and 7 other objectives. + amount = random.randrange(2, 7) + for i in range(amount): + # 1 in 4 additional objectives are AA. + if random.randint(0, 3) == 0: + self.generate_aa_site() + else: + self.generate_ground_point() + + def generate_ground_point(self) -> None: + try: + category = random.choice(self.faction.building_set) + except IndexError: + logging.exception("Faction has no buildings defined") + return + + obj_name = namegen.random_objective_name() + template = random.choice(list(self.templates[category].values())) + point = find_location(category != "oil", + self.control_point.position, + self.game.theater, 10000, 40000, + self.control_point.ground_objects) + + if point is None: + logging.error( + f"Could not find point for {obj_name} at {self.control_point}") + return + + object_id = 0 + group_id = self.game.next_group_id() + + # TODO: Create only one TGO per objective, each with multiple units. + for unit in template: + object_id += 1 + + template_point = Point(unit["offset"].x, unit["offset"].y) + g = BuildingGroundObject( + obj_name, category, group_id, object_id, point + template_point, + unit["heading"], self.control_point, unit["type"]) + + self.control_point.connected_objectives.append(g) + + def generate_aa_site(self) -> None: + obj_name = namegen.random_objective_name() + position = find_location(True, self.control_point.position, + self.game.theater, 10000, 40000, + self.control_point.ground_objects) + + if position is None: + logging.error( + f"Could not find point for {obj_name} at {self.control_point}") + return + + group_id = self.game.next_group_id() + + g = SamGroundObject(namegen.random_objective_name(), group_id, + position, self.control_point, for_airbase=False) + group = generate_anti_air_group(self.game, g, self.faction_name) + if group is not None: + g.groups = [group] + self.control_point.connected_objectives.append(g) + + def generate_missile_sites(self) -> None: + for i in range(self.faction.missiles_group_count): + self.generate_missile_site() + + def generate_missile_site(self) -> None: + point = find_location(True, self.control_point.position, + self.game.theater, 2500, 40000, [], False) + if point is None: + logging.info( + f"Could not find point for {self.control_point} missile site") + return + + group_id = self.game.next_group_id() + + g = MissileSiteGroundObject(namegen.random_objective_name(), group_id, + point, self.control_point) + group = generate_missile_group(self.game, g, self.faction_name) + g.groups = [] + if group is not None: + g.groups.append(group) + self.control_point.connected_objectives.append(g) + return class GroundObjectGenerator: @@ -424,11 +437,9 @@ class GroundObjectGenerator: def generate_for_control_point(self, control_point: ControlPoint) -> bool: generator: ControlPointGroundObjectGenerator if control_point.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP: - generator = CarrierGroundObjectGenerator(self.game, control_point, - self.templates) + generator = CarrierGroundObjectGenerator(self.game, control_point) elif control_point.cptype == ControlPointType.LHA_GROUP: - generator = LhaGroundObjectGenerator(self.game, control_point, - self.templates) + generator = LhaGroundObjectGenerator(self.game, control_point) else: generator = AirbaseGroundObjectGenerator(self.game, control_point, self.templates) @@ -436,8 +447,8 @@ class GroundObjectGenerator: def generate_airbase_defense_group(airbase_defense_group_id: int, - ground_obj: TheaterGroundObject, - faction: str, game: Game) -> None: + ground_obj: SamGroundObject, faction: str, + game: Game) -> None: if airbase_defense_group_id == 0: group = generate_armor_group(faction, game, ground_obj) elif airbase_defense_group_id == 1 and random.randint(0, 1) == 0: diff --git a/theater/theatergroundobject.py b/theater/theatergroundobject.py index b9010da9..47b022fa 100644 --- a/theater/theatergroundobject.py +++ b/theater/theatergroundobject.py @@ -8,7 +8,6 @@ from dcs.unit import Unit from dcs.unitgroup import Group if TYPE_CHECKING: - from .conflicttheater import ConflictTheater from .controlpoint import ControlPoint from .missiontarget import MissionTarget @@ -72,29 +71,21 @@ CATEGORY_MAP = { class TheaterGroundObject(MissionTarget): - def __init__(self, name: str, category: str, group_id: int, object_id: int, - position: Point, heading: int, cp_id: int, dcs_identifier: str, + def __init__(self, name: str, category: str, group_id: int, position: Point, + heading: int, control_point: ControlPoint, dcs_identifier: str, airbase_group: bool, sea_object: bool) -> None: super().__init__(name, position) self.category = category self.group_id = group_id - self.object_id = object_id self.heading = heading - self.cp_id = cp_id + self.control_point = control_point self.dcs_identifier = dcs_identifier self.airbase_group = airbase_group self.sea_object = sea_object self.is_dead = False + # TODO: There is never more than one group. self.groups: List[Group] = [] - @property - def string_identifier(self): - return "{}|{}|{}|{}".format(self.category, self.cp_id, self.group_id, self.object_id) - - @property - def group_identifier(self) -> str: - return "{}|{}".format(self.category, self.group_id) - @property def units(self) -> List[Unit]: """ @@ -103,26 +94,20 @@ class TheaterGroundObject(MissionTarget): return list(itertools.chain.from_iterable([g.units for g in self.groups])) @property - def name_abbrev(self) -> str: - return ABBREV_NAME[self.category] + def group_name(self) -> str: + """The name of the unit group.""" + return f"{self.category}|{self.group_id}" def __str__(self) -> str: return NAME_BY_CATEGORY[self.category] - def matches_string_identifier(self, identifier): - return self.string_identifier == identifier + def is_same_group(self, identifier: str) -> bool: + return self.group_id == identifier @property def obj_name(self) -> str: return self.name - def parent_control_point(self, theater: ConflictTheater) -> ControlPoint: - """Searches the theater for the parent control point.""" - for cp in theater.controlpoints: - if cp.id == self.cp_id: - return cp - raise RuntimeError("Could not find matching control point in theater") - class BuildingGroundObject(TheaterGroundObject): def __init__(self, name: str, category: str, group_id: int, object_id: int, @@ -132,14 +117,19 @@ class BuildingGroundObject(TheaterGroundObject): name=name, category=category, group_id=group_id, - object_id=object_id, position=position, heading=heading, - cp_id=control_point.id, + control_point=control_point, dcs_identifier=dcs_identifier, airbase_group=False, sea_object=False ) + self.object_id = object_id + + @property + def group_name(self) -> str: + """The name of the unit group.""" + return f"{self.category}|{self.group_id}|{self.object_id}" class GenericCarrierGroundObject(TheaterGroundObject): @@ -154,10 +144,9 @@ class CarrierGroundObject(GenericCarrierGroundObject): name=name, category="CARRIER", group_id=group_id, - object_id=0, position=control_point.position, heading=0, - cp_id=control_point.id, + control_point=control_point, dcs_identifier="CARRIER", airbase_group=True, sea_object=True @@ -172,10 +161,9 @@ class LhaGroundObject(GenericCarrierGroundObject): name=name, category="LHA", group_id=group_id, - object_id=0, position=control_point.position, heading=0, - cp_id=control_point.id, + control_point=control_point, dcs_identifier="LHA", airbase_group=True, sea_object=True @@ -189,16 +177,18 @@ class MissileSiteGroundObject(TheaterGroundObject): name=name, category="aa", group_id=group_id, - object_id=0, position=position, heading=0, - cp_id=control_point.id, + control_point=control_point, dcs_identifier="AA", airbase_group=False, sea_object=False ) +# TODO: Differentiate types. +# This type gets used both for AA sites (SAM, AAA, or SHORAD) but also for the +# armor garrisons at airbases. These should each be split into their own types. class SamGroundObject(TheaterGroundObject): def __init__(self, name: str, group_id: int, position: Point, control_point: ControlPoint, for_airbase: bool) -> None: @@ -206,14 +196,26 @@ class SamGroundObject(TheaterGroundObject): name=name, category="aa", group_id=group_id, - object_id=0, position=position, heading=0, - cp_id=control_point.id, + control_point=control_point, dcs_identifier="AA", airbase_group=for_airbase, sea_object=False ) + # Set by the SAM unit generator if the generated group is compatible + # with Skynet. + self.skynet_capable = False + + @property + def group_name(self) -> str: + if self.skynet_capable: + # Prefix the group names of SAM sites with the side color so Skynet + # can find them. + color = "BLUE" if self.control_point.captured else "RED" + return f"{color}|SAM|{self.group_id}" + else: + return super().group_name class ShipGroundObject(TheaterGroundObject): @@ -223,10 +225,9 @@ class ShipGroundObject(TheaterGroundObject): name=name, category="aa", group_id=group_id, - object_id=0, position=position, heading=0, - cp_id=control_point.id, + control_point=control_point, dcs_identifier="AA", airbase_group=False, sea_object=True