From 4cf406aefae4f094d41f7cb901a1f03d51b570ea Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 7 Nov 2020 12:35:23 -0800 Subject: [PATCH 1/6] Defer game load to after UI initialization. This removes both the double load that happens on startup and also makes it possible to get the UI to create a new game if the existing default.liberation is not compatible. --- qt_ui/widgets/map/QLiberationMap.py | 2 +- qt_ui/windows/QLiberationWindow.py | 49 +++++++++++++++---------- resources/stylesheets/style-dcs.css | 8 ++++ resources/stylesheets/windows-style.css | 8 ++++ 4 files changed, 46 insertions(+), 21 deletions(-) diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 1017619b..c03290f4 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/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 From 853ee5aac4ec96e8d7c1da387108a3bdd659e74f Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 6 Nov 2020 18:06:42 -0800 Subject: [PATCH 2/6] Cleanup Theater Ground Object. A whole bunch of this data is redundant. --- game/debriefing.py | 4 +- game/event/event.py | 4 +- gen/aircraft.py | 8 +-- gen/groundobjectsgen.py | 4 +- gen/sam/group_generator.py | 14 ++--- qt_ui/widgets/combos/QFlightTypeComboBox.py | 2 +- .../QPredefinedWaypointSelectionComboBox.py | 4 +- theater/theatergroundobject.py | 60 ++++++++----------- 8 files changed, 43 insertions(+), 57 deletions(-) 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/group_generator.py b/gen/sam/group_generator.py index f35f1a0e..1bd57939 100644 --- a/gen/sam/group_generator.py +++ b/gen/sam/group_generator.py @@ -42,11 +42,10 @@ class GroupGenerator: 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 +87,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): @@ -100,11 +100,9 @@ class ShipGroupGenerator(GroupGenerator): 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/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/theater/theatergroundobject.py b/theater/theatergroundobject.py index b9010da9..34a98c87 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,10 +196,9 @@ 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 @@ -223,10 +212,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 From 56b51c85bbb6f2b5e6f78504996751a009601464 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 6 Nov 2020 18:08:40 -0800 Subject: [PATCH 3/6] Fix targeting SAMs with strike missions. The changes for Skynet unfortunately broke this because the names used by the TGO and the name of the group itself were no longer in sync. This deserves a larger cleanup where we decouple that naming requirement (TGOs don't need a lot of the data they currently have), but this works until we have the time to do that. Fixes https://github.com/Khopa/dcs_liberation/issues/298 --- gen/sam/genericsam_group_generator.py | 18 +++++++----------- gen/sam/group_generator.py | 13 +++++-------- gen/sam/sam_group_generator.py | 5 +++-- .../plugins/skynetiads/skynetiads-config.lua | 4 ++-- theater/start_generator.py | 4 ++-- theater/theatergroundobject.py | 13 +++++++++++++ 6 files changed, 32 insertions(+), 25 deletions(-) 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 1bd57939..94738eef 100644 --- a/gen/sam/group_generator.py +++ b/gen/sam/group_generator.py @@ -22,20 +22,16 @@ 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 @@ -96,7 +92,8 @@ 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 diff --git a/gen/sam/sam_group_generator.py b/gen/sam/sam_group_generator.py index 6acc2759..f0549718 100644 --- a/gen/sam/sam_group_generator.py +++ b/gen/sam/sam_group_generator.py @@ -36,6 +36,7 @@ from gen.sam.sam_zu23 import ZU23Generator from gen.sam.sam_zu23_ural import ZU23UralGenerator from gen.sam.sam_zu23_ural_insurgent import ZU23UralInsurgentGenerator from theater import TheaterGroundObject +from theater.theatergroundobject import SamGroundObject SAM_MAP = { "HawkGenerator": HawkGenerator, @@ -125,13 +126,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/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/theater/start_generator.py b/theater/start_generator.py index 9e679f6c..536b7680 100644 --- a/theater/start_generator.py +++ b/theater/start_generator.py @@ -426,8 +426,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 34a98c87..47b022fa 100644 --- a/theater/theatergroundobject.py +++ b/theater/theatergroundobject.py @@ -203,6 +203,19 @@ class SamGroundObject(TheaterGroundObject): 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): From c2ee169d16d61a713c649e21451a07d403e9f96c Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 6 Nov 2020 18:11:08 -0800 Subject: [PATCH 4/6] Fix regeneration of base defenses on capture. I messed up the counting here and was counting *every* object rather than just the base defenses, so it was very unlikely that we'd hit base defenses on the second object, which is the only index valid for SAM generation. That logic maybe needs to be different, but this restores the previous behavior. --- theater/controlpoint.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/theater/controlpoint.py b/theater/controlpoint.py index 4aaedf21..7c8963c6 100644 --- a/theater/controlpoint.py +++ b/theater/controlpoint.py @@ -17,7 +17,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 @@ -226,8 +226,16 @@ class ControlPoint(MissionTarget): # Handle cyclic dependency. from .start_generator import generate_airbase_defense_group - for idx, ground_object in enumerate(self.ground_objects): + base_defense_idx = 0 + for ground_object in self.ground_objects: + if not isinstance(ground_object, SamGroundObject): + continue + if not ground_object.airbase_group: + continue + + # Reset in case we replace the SAM with something else. + ground_object.skynet_capable = False ground_object.groups = [] - if ground_object.airbase_group and faction_name != "": - generate_airbase_defense_group(idx, ground_object, - faction_name, game) + generate_airbase_defense_group(base_defense_idx, ground_object, + faction_name, game) + base_defense_idx += 1 From 3979ee57ffa526da9201e3e2edb96c0d35fbb9d3 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 6 Nov 2020 18:36:07 -0800 Subject: [PATCH 5/6] Cleanup oddities of CP generation. * Don't spawn missile sites or ground locations for CVs/LHAs. * Don't spawn ground locations around CVs/LHAs. * Spawn AA sites even if the faction has no buildings defined. --- theater/start_generator.py | 223 +++++++++++++++++++------------------ 1 file changed, 112 insertions(+), 111 deletions(-) diff --git a/theater/start_generator.py b/theater/start_generator.py index 536b7680..4a4eacb8 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: @@ -179,7 +177,6 @@ class ControlPointGroundObjectGenerator: def generate(self) -> bool: self.control_point.ground_objects = [] - self.generate_ground_points() 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: @@ -297,29 +217,6 @@ class ControlPointGroundObjectGenerator: 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 - class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator): def generate(self) -> bool: @@ -372,15 +269,121 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator): 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 - for i in range(random.randint(3, 6)): - self.generate_sam(i) + self.generate_base_defenses() + self.generate_ground_points() + + if self.faction.missiles: + self.generate_missile_sites() + return True - def generate_sam(self, index: int) -> None: + 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.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_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 + + def generate_base_defenses(self) -> None: + for i in range(random.randint(3, 6)): + self.generate_base_defense(i) + + def generate_base_defense(self, index: int) -> None: position = find_location(True, self.control_point.position, self.game.theater, 800, 3200, [], True) if position is None: @@ -414,11 +417,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) From 8bc69415a751863f03e857230386235c5f05cbde Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 7 Nov 2020 12:25:59 -0800 Subject: [PATCH 6/6] Clean up base defense generation. --- theater/controlpoint.py | 26 +++++++-------- theater/start_generator.py | 66 ++++++++++++++++++++++---------------- 2 files changed, 49 insertions(+), 43 deletions(-) diff --git a/theater/controlpoint.py b/theater/controlpoint.py index 7c8963c6..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 @@ -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,17 +232,6 @@ class ControlPoint(MissionTarget): self.base.armor = {} # Handle cyclic dependency. - from .start_generator import generate_airbase_defense_group - base_defense_idx = 0 - for ground_object in self.ground_objects: - if not isinstance(ground_object, SamGroundObject): - continue - if not ground_object.airbase_group: - continue - - # Reset in case we replace the SAM with something else. - ground_object.skynet_capable = False - ground_object.groups = [] - generate_airbase_defense_group(base_defense_idx, ground_object, - faction_name, game) - base_defense_idx += 1 + 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 4a4eacb8..e2fd1907 100644 --- a/theater/start_generator.py +++ b/theater/start_generator.py @@ -176,7 +176,7 @@ class ControlPointGroundObjectGenerator: return db.FACTIONS[self.faction_name] def generate(self) -> bool: - self.control_point.ground_objects = [] + 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 @@ -215,7 +215,7 @@ class 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) class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator): @@ -238,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 @@ -263,11 +263,40 @@ 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 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_base_defense(i) + + def generate_base_defense(self, index: int) -> None: + position = find_location(True, self.control_point.position, + self.game.theater, 800, 3200, [], True) + if position is None: + logging.error("Could not find position for " + f"{self.control_point} base defense") + return + + group_id = self.game.next_group_id() + + 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.base_defenses.append(g) + + class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): def __init__(self, game: Game, control_point: ControlPoint, templates: GroundObjectTemplates) -> None: @@ -278,7 +307,8 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): if not super().generate(): return False - self.generate_base_defenses() + BaseDefenseGenerator(self.game, self.control_point, + self.faction_name).generate() self.generate_ground_points() if self.faction.missiles: @@ -334,7 +364,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): obj_name, category, group_id, object_id, point + template_point, unit["heading"], self.control_point, unit["type"]) - self.control_point.ground_objects.append(g) + self.control_point.connected_objectives.append(g) def generate_aa_site(self) -> None: obj_name = namegen.random_objective_name() @@ -354,7 +384,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): 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) + self.control_point.connected_objectives.append(g) def generate_missile_sites(self) -> None: for i in range(self.faction.missiles_group_count): @@ -376,29 +406,9 @@ class AirbaseGroundObjectGenerator(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) return - def generate_base_defenses(self) -> None: - for i in range(random.randint(3, 6)): - self.generate_base_defense(i) - - def generate_base_defense(self, index: int) -> None: - position = find_location(True, self.control_point.position, - self.game.theater, 800, 3200, [], True) - if position is None: - logging.error("Could not find position for " - f"{self.control_point} base defense") - return - - group_id = self.game.next_group_id() - - 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) - class GroundObjectGenerator: def __init__(self, game: Game) -> None: