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