mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Adds NASAM launcher B and C to the list of units that can be placed in a campaign template to define a MERAD spawn location. This is needed to maintain compatibility with the new versions of my campaigns. Pull requests should be made against the `develop` branch. Any backports necessary will be handled by the development team. Pull requests should be focused on one task. Multiple bug fixes should be multiple PRs. We cannot merge half a PR, and combined PRs are much more difficult to review. PRs that do not adhere to this will have their review delayed. Prefer rebase to merge, and squash commits as needed to preserve a readable commit history. This project maintains linear history in the develop branch, so we will either rebase or squash your PR when merging. It is much easier for us if your branch already has a readable commit history (ensure that your commit subject lines are clear enough to identify the patch in the git log). An exception to this is made for large PRs that are likely to require multiple rounds of review; in that case it's easier if you **don't** do this (GitHub does not preserve the history of old commits, so we cannot filter a PR for only new changes if a branch is force pushed) and we will squash it when merging. New features and bug fixes are usually worth mentioning in the changelog. Exceptions are fixes for bugs that never shipped (were only present in a canary build), and changes with no intended user observable behavior, such as a refactor. If you're comfortable writing the note yourself, add it to `changelog.md` in the root of the project in the section for the upcoming release. Co-authored-by: Starfire13 <72491792+Starfire13@users.noreply.github.com>
462 lines
18 KiB
Python
462 lines
18 KiB
Python
from __future__ import annotations
|
|
|
|
import itertools
|
|
from functools import cached_property
|
|
from pathlib import Path
|
|
from typing import Iterator, List, TYPE_CHECKING
|
|
from uuid import UUID
|
|
|
|
from dcs import Mission
|
|
from dcs.countries import CombinedJointTaskForcesBlue, CombinedJointTaskForcesRed
|
|
from dcs.country import Country
|
|
from dcs.planes import F_15C
|
|
from dcs.ships import HandyWind, LHA_Tarawa, Stennis, USS_Arleigh_Burke_IIa
|
|
from dcs.statics import Fortification, Warehouse
|
|
from dcs.unitgroup import PlaneGroup, ShipGroup, StaticGroup, VehicleGroup
|
|
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
|
|
|
|
from game.campaignloader.controlpointbuilder import ControlPointBuilder
|
|
from game.campaignloader.controlpointconfig import ControlPointConfig
|
|
from game.profiling import logged_duration
|
|
from game.scenery_group import SceneryGroup
|
|
from game.theater.controlpoint import ControlPoint
|
|
from game.theater.presetlocation import PresetLocation
|
|
|
|
if TYPE_CHECKING:
|
|
from game.theater.conflicttheater import ConflictTheater
|
|
|
|
|
|
class MizCampaignLoader:
|
|
BLUE_COUNTRY = CombinedJointTaskForcesBlue()
|
|
RED_COUNTRY = CombinedJointTaskForcesRed()
|
|
|
|
OFF_MAP_UNIT_TYPE = F_15C.id
|
|
|
|
CV_UNIT_TYPE = Stennis.id
|
|
LHA_UNIT_TYPE = LHA_Tarawa.id
|
|
FRONT_LINE_UNIT_TYPE = Armor.M_113.id
|
|
SHIPPING_LANE_UNIT_TYPE = HandyWind.id
|
|
|
|
FOB_UNIT_TYPE = Unarmed.SKP_11.id
|
|
FARP_HELIPADS_TYPE = ["Invisible FARP", "SINGLE_HELIPAD"]
|
|
|
|
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
|
|
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
|
|
MISSILE_SITE_UNIT_TYPE = MissilesSS.Scud_B.id
|
|
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.Hy_launcher.id
|
|
|
|
COMMAND_CENTER_UNIT_TYPE = Fortification._Command_Center.id
|
|
CONNECTION_NODE_UNIT_TYPE = Fortification.Comms_tower_M.id
|
|
POWER_SOURCE_UNIT_TYPE = Fortification.GeneratorF.id
|
|
|
|
# Multiple options for air defenses so campaign designers can more accurately see
|
|
# the coverage of their IADS for the expected type.
|
|
LONG_RANGE_SAM_UNIT_TYPES = {
|
|
AirDefence.Patriot_ln.id,
|
|
AirDefence.S_300PS_5P85C_ln.id,
|
|
AirDefence.S_300PS_5P85D_ln.id,
|
|
}
|
|
|
|
MEDIUM_RANGE_SAM_UNIT_TYPES = {
|
|
AirDefence.Hawk_ln.id,
|
|
AirDefence.S_75M_Volhov.id,
|
|
AirDefence.X_5p73_s_125_ln.id,
|
|
AirDefence.NASAMS_LN_B.id,
|
|
AirDefence.NASAMS_LN_C.id,
|
|
}
|
|
|
|
SHORT_RANGE_SAM_UNIT_TYPES = {
|
|
AirDefence.M1097_Avenger.id,
|
|
AirDefence.Rapier_fsa_launcher.id,
|
|
AirDefence.X_2S6_Tunguska.id,
|
|
AirDefence.Strela_1_9P31.id,
|
|
}
|
|
|
|
AAA_UNIT_TYPES = {
|
|
AirDefence.Flak18.id,
|
|
AirDefence.Vulcan.id,
|
|
AirDefence.ZSU_23_4_Shilka.id,
|
|
}
|
|
|
|
EWR_UNIT_TYPE = AirDefence.X_1L13_EWR.id
|
|
|
|
ARMOR_GROUP_UNIT_TYPE = Armor.M_1_Abrams.id
|
|
|
|
FACTORY_UNIT_TYPE = Fortification.Workshop_A.id
|
|
|
|
AMMUNITION_DEPOT_UNIT_TYPE = Warehouse._Ammunition_depot.id
|
|
|
|
STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id
|
|
|
|
def __init__(
|
|
self,
|
|
miz: Path,
|
|
theater: ConflictTheater,
|
|
control_point_configs: dict[str | int, ControlPointConfig],
|
|
) -> None:
|
|
self.theater = theater
|
|
self.control_point_builder = ControlPointBuilder(theater, control_point_configs)
|
|
self.mission = Mission()
|
|
with logged_duration("Loading miz"):
|
|
self.mission.load_file(str(miz))
|
|
|
|
# If there are no red carriers there usually aren't red units. Make sure
|
|
# both countries are initialized so we don't have to deal with None.
|
|
if self.mission.country(self.BLUE_COUNTRY.name) is None:
|
|
self.mission.coalition["blue"].add_country(self.BLUE_COUNTRY)
|
|
if self.mission.country(self.RED_COUNTRY.name) is None:
|
|
self.mission.coalition["red"].add_country(self.RED_COUNTRY)
|
|
|
|
def country(self, blue: bool) -> Country:
|
|
country = self.mission.country(
|
|
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name
|
|
)
|
|
# Should be guaranteed because we initialized them.
|
|
assert country
|
|
return country
|
|
|
|
@property
|
|
def blue(self) -> Country:
|
|
return self.country(blue=True)
|
|
|
|
@property
|
|
def red(self) -> Country:
|
|
return self.country(blue=False)
|
|
|
|
def off_map_spawns(self, blue: bool) -> Iterator[PlaneGroup]:
|
|
for group in self.country(blue).plane_group:
|
|
if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
|
|
yield group
|
|
|
|
def carriers(self, blue: bool) -> Iterator[ShipGroup]:
|
|
for group in self.country(blue).ship_group:
|
|
if group.units[0].type == self.CV_UNIT_TYPE:
|
|
yield group
|
|
|
|
def lhas(self, blue: bool) -> Iterator[ShipGroup]:
|
|
for group in self.country(blue).ship_group:
|
|
if group.units[0].type == self.LHA_UNIT_TYPE:
|
|
yield group
|
|
|
|
def fobs(self, blue: bool) -> Iterator[VehicleGroup]:
|
|
for group in self.country(blue).vehicle_group:
|
|
if group.units[0].type == self.FOB_UNIT_TYPE:
|
|
yield group
|
|
|
|
@property
|
|
def ships(self) -> Iterator[ShipGroup]:
|
|
for group in self.red.ship_group:
|
|
if group.units[0].type == self.SHIP_UNIT_TYPE:
|
|
yield group
|
|
|
|
@property
|
|
def offshore_strike_targets(self) -> Iterator[StaticGroup]:
|
|
for group in self.red.static_group:
|
|
if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE:
|
|
yield group
|
|
|
|
@property
|
|
def missile_sites(self) -> Iterator[VehicleGroup]:
|
|
for group in self.red.vehicle_group:
|
|
if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE:
|
|
yield group
|
|
|
|
@property
|
|
def coastal_defenses(self) -> Iterator[VehicleGroup]:
|
|
for group in self.red.vehicle_group:
|
|
if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE:
|
|
yield group
|
|
|
|
@property
|
|
def long_range_sams(self) -> Iterator[VehicleGroup]:
|
|
for group in self.red.vehicle_group:
|
|
if group.units[0].type in self.LONG_RANGE_SAM_UNIT_TYPES:
|
|
yield group
|
|
|
|
@property
|
|
def medium_range_sams(self) -> Iterator[VehicleGroup]:
|
|
for group in self.red.vehicle_group:
|
|
if group.units[0].type in self.MEDIUM_RANGE_SAM_UNIT_TYPES:
|
|
yield group
|
|
|
|
@property
|
|
def short_range_sams(self) -> Iterator[VehicleGroup]:
|
|
for group in self.red.vehicle_group:
|
|
if group.units[0].type in self.SHORT_RANGE_SAM_UNIT_TYPES:
|
|
yield group
|
|
|
|
@property
|
|
def aaa(self) -> Iterator[VehicleGroup]:
|
|
for group in itertools.chain(self.blue.vehicle_group, self.red.vehicle_group):
|
|
if group.units[0].type in self.AAA_UNIT_TYPES:
|
|
yield group
|
|
|
|
@property
|
|
def ewrs(self) -> Iterator[VehicleGroup]:
|
|
for group in self.red.vehicle_group:
|
|
if group.units[0].type in self.EWR_UNIT_TYPE:
|
|
yield group
|
|
|
|
@property
|
|
def armor_groups(self) -> Iterator[VehicleGroup]:
|
|
for group in itertools.chain(self.blue.vehicle_group, self.red.vehicle_group):
|
|
if group.units[0].type in self.ARMOR_GROUP_UNIT_TYPE:
|
|
yield group
|
|
|
|
@property
|
|
def helipads(self) -> Iterator[StaticGroup]:
|
|
for group in itertools.chain(self.blue.static_group, self.red.static_group):
|
|
if group.units[0].type in self.FARP_HELIPADS_TYPE:
|
|
yield group
|
|
|
|
@property
|
|
def factories(self) -> Iterator[StaticGroup]:
|
|
for group in self.blue.static_group:
|
|
if group.units[0].type in self.FACTORY_UNIT_TYPE:
|
|
yield group
|
|
|
|
@property
|
|
def ammunition_depots(self) -> Iterator[StaticGroup]:
|
|
for group in itertools.chain(self.blue.static_group, self.red.static_group):
|
|
if group.units[0].type in self.AMMUNITION_DEPOT_UNIT_TYPE:
|
|
yield group
|
|
|
|
@property
|
|
def strike_targets(self) -> Iterator[StaticGroup]:
|
|
for group in itertools.chain(self.blue.static_group, self.red.static_group):
|
|
if group.units[0].type in self.STRIKE_TARGET_UNIT_TYPE:
|
|
yield group
|
|
|
|
@property
|
|
def scenery(self) -> List[SceneryGroup]:
|
|
return SceneryGroup.from_trigger_zones(z for z in self.mission.triggers.zones())
|
|
|
|
@cached_property
|
|
def control_points(self) -> dict[UUID, ControlPoint]:
|
|
control_points: dict[UUID, ControlPoint] = {}
|
|
control_point: ControlPoint
|
|
for airport in self.mission.terrain.airport_list():
|
|
if airport.is_blue() or airport.is_red():
|
|
control_point = self.control_point_builder.create_airfield(airport)
|
|
control_points[control_point.id] = control_point
|
|
|
|
for blue in (False, True):
|
|
for group in self.off_map_spawns(blue):
|
|
control_point = self.control_point_builder.create_off_map(
|
|
str(group.name),
|
|
group.position,
|
|
self.theater,
|
|
starts_blue=blue,
|
|
captured_invert=group.late_activation,
|
|
)
|
|
control_points[control_point.id] = control_point
|
|
for ship in self.carriers(blue):
|
|
control_point = self.control_point_builder.create_carrier(
|
|
ship.name,
|
|
ship.position,
|
|
self.theater,
|
|
starts_blue=blue,
|
|
captured_invert=ship.late_activation,
|
|
)
|
|
control_points[control_point.id] = control_point
|
|
for ship in self.lhas(blue):
|
|
control_point = self.control_point_builder.create_lha(
|
|
ship.name,
|
|
ship.position,
|
|
self.theater,
|
|
starts_blue=blue,
|
|
captured_invert=ship.late_activation,
|
|
)
|
|
control_points[control_point.id] = control_point
|
|
for fob in self.fobs(blue):
|
|
control_point = self.control_point_builder.create_fob(
|
|
str(fob.name),
|
|
fob.position,
|
|
self.theater,
|
|
starts_blue=blue,
|
|
captured_invert=fob.late_activation,
|
|
)
|
|
control_points[control_point.id] = control_point
|
|
|
|
return control_points
|
|
|
|
@property
|
|
def front_line_path_groups(self) -> Iterator[VehicleGroup]:
|
|
for group in self.country(blue=True).vehicle_group:
|
|
if group.units[0].type == self.FRONT_LINE_UNIT_TYPE:
|
|
yield group
|
|
|
|
@property
|
|
def shipping_lane_groups(self) -> Iterator[ShipGroup]:
|
|
for group in self.country(blue=True).ship_group:
|
|
if group.units[0].type == self.SHIPPING_LANE_UNIT_TYPE:
|
|
yield group
|
|
|
|
@property
|
|
def iads_command_centers(self) -> Iterator[StaticGroup]:
|
|
for group in itertools.chain(self.blue.static_group, self.red.static_group):
|
|
if group.units[0].type in self.COMMAND_CENTER_UNIT_TYPE:
|
|
yield group
|
|
|
|
@property
|
|
def iads_connection_nodes(self) -> Iterator[StaticGroup]:
|
|
for group in itertools.chain(self.blue.static_group, self.red.static_group):
|
|
if group.units[0].type in self.CONNECTION_NODE_UNIT_TYPE:
|
|
yield group
|
|
|
|
@property
|
|
def iads_power_sources(self) -> Iterator[StaticGroup]:
|
|
for group in itertools.chain(self.blue.static_group, self.red.static_group):
|
|
if group.units[0].type in self.POWER_SOURCE_UNIT_TYPE:
|
|
yield group
|
|
|
|
def add_supply_routes(self) -> None:
|
|
for group in self.front_line_path_groups:
|
|
# The unit will have its first waypoint at the source CP and the final
|
|
# waypoint at the destination CP. Each waypoint defines the path of the
|
|
# cargo ship.
|
|
waypoints = [p.position for p in group.points]
|
|
origin = self.theater.closest_control_point(waypoints[0])
|
|
if origin is None:
|
|
raise RuntimeError(
|
|
f"No control point near the first waypoint of {group.name}"
|
|
)
|
|
destination = self.theater.closest_control_point(waypoints[-1])
|
|
if destination is None:
|
|
raise RuntimeError(
|
|
f"No control point near the final waypoint of {group.name}"
|
|
)
|
|
|
|
self.control_points[origin.id].create_convoy_route(destination, waypoints)
|
|
self.control_points[destination.id].create_convoy_route(
|
|
origin, list(reversed(waypoints))
|
|
)
|
|
|
|
def add_shipping_lanes(self) -> None:
|
|
for group in self.shipping_lane_groups:
|
|
# The unit will have its first waypoint at the source CP and the final
|
|
# waypoint at the destination CP. Each waypoint defines the path of the
|
|
# cargo ship.
|
|
waypoints = [p.position for p in group.points]
|
|
origin = self.theater.closest_control_point(waypoints[0])
|
|
if origin is None:
|
|
raise RuntimeError(
|
|
f"No control point near the first waypoint of {group.name}"
|
|
)
|
|
destination = self.theater.closest_control_point(waypoints[-1])
|
|
if destination is None:
|
|
raise RuntimeError(
|
|
f"No control point near the final waypoint of {group.name}"
|
|
)
|
|
|
|
self.control_points[origin.id].create_shipping_lane(destination, waypoints)
|
|
self.control_points[destination.id].create_shipping_lane(
|
|
origin, list(reversed(waypoints))
|
|
)
|
|
|
|
def add_preset_locations(self) -> None:
|
|
for static in self.offshore_strike_targets:
|
|
closest = self.theater.closest_control_point(static.position)
|
|
closest.preset_locations.offshore_strike_locations.append(
|
|
PresetLocation.from_group(static)
|
|
)
|
|
|
|
for ship in self.ships:
|
|
closest = self.theater.closest_control_point(
|
|
ship.position, allow_naval=True
|
|
)
|
|
closest.preset_locations.ships.append(PresetLocation.from_group(ship))
|
|
|
|
for group in self.missile_sites:
|
|
closest = self.theater.closest_control_point(group.position)
|
|
closest.preset_locations.missile_sites.append(
|
|
PresetLocation.from_group(group)
|
|
)
|
|
|
|
for group in self.coastal_defenses:
|
|
closest = self.theater.closest_control_point(group.position)
|
|
closest.preset_locations.coastal_defenses.append(
|
|
PresetLocation.from_group(group)
|
|
)
|
|
|
|
for group in self.long_range_sams:
|
|
closest = self.theater.closest_control_point(group.position)
|
|
closest.preset_locations.long_range_sams.append(
|
|
PresetLocation.from_group(group)
|
|
)
|
|
|
|
for group in self.medium_range_sams:
|
|
closest = self.theater.closest_control_point(group.position)
|
|
closest.preset_locations.medium_range_sams.append(
|
|
PresetLocation.from_group(group)
|
|
)
|
|
|
|
for group in self.short_range_sams:
|
|
closest = self.theater.closest_control_point(group.position)
|
|
closest.preset_locations.short_range_sams.append(
|
|
PresetLocation.from_group(group)
|
|
)
|
|
|
|
for group in self.aaa:
|
|
closest = self.theater.closest_control_point(group.position)
|
|
closest.preset_locations.aaa.append(PresetLocation.from_group(group))
|
|
|
|
for group in self.ewrs:
|
|
closest = self.theater.closest_control_point(group.position)
|
|
closest.preset_locations.ewrs.append(PresetLocation.from_group(group))
|
|
|
|
for group in self.armor_groups:
|
|
closest = self.theater.closest_control_point(group.position)
|
|
closest.preset_locations.armor_groups.append(
|
|
PresetLocation.from_group(group)
|
|
)
|
|
|
|
for static in self.helipads:
|
|
closest = self.theater.closest_control_point(static.position)
|
|
closest.helipads.append(PresetLocation.from_group(static))
|
|
|
|
for static in self.factories:
|
|
closest = self.theater.closest_control_point(static.position)
|
|
closest.preset_locations.factories.append(PresetLocation.from_group(static))
|
|
|
|
for static in self.ammunition_depots:
|
|
closest = self.theater.closest_control_point(static.position)
|
|
closest.preset_locations.ammunition_depots.append(
|
|
PresetLocation.from_group(static)
|
|
)
|
|
|
|
for static in self.strike_targets:
|
|
closest = self.theater.closest_control_point(static.position)
|
|
closest.preset_locations.strike_locations.append(
|
|
PresetLocation.from_group(static)
|
|
)
|
|
|
|
for iads_command_center in self.iads_command_centers:
|
|
closest = self.theater.closest_control_point(iads_command_center.position)
|
|
closest.preset_locations.iads_command_center.append(
|
|
PresetLocation.from_group(iads_command_center)
|
|
)
|
|
|
|
for iads_connection_node in self.iads_connection_nodes:
|
|
closest = self.theater.closest_control_point(iads_connection_node.position)
|
|
closest.preset_locations.iads_connection_node.append(
|
|
PresetLocation.from_group(iads_connection_node)
|
|
)
|
|
|
|
for iads_power_source in self.iads_power_sources:
|
|
closest = self.theater.closest_control_point(iads_power_source.position)
|
|
closest.preset_locations.iads_power_source.append(
|
|
PresetLocation.from_group(iads_power_source)
|
|
)
|
|
|
|
for scenery_group in self.scenery:
|
|
closest = self.theater.closest_control_point(scenery_group.centroid)
|
|
closest.preset_locations.scenery.append(scenery_group)
|
|
|
|
def populate_theater(self) -> None:
|
|
for control_point in self.control_points.values():
|
|
self.theater.add_controlpoint(control_point)
|
|
self.add_preset_locations()
|
|
self.add_supply_routes()
|
|
self.add_shipping_lanes()
|