class MizCampaignLoader: BLUE_COUNTRY = CombinedJointTaskForcesBlue() RED_COUNTRY = CombinedJointTaskForcesRed() OFF_MAP_UNIT_TYPE = F_15C.id CV_UNIT_TYPE = CVN_74_John_C__Stennis.id LHA_UNIT_TYPE = LHA_1_Tarawa.id FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id FOB_UNIT_TYPE = Unarmed.CP_SKP_11_ATC_Mobile_Command_Post.id EWR_UNIT_TYPE = AirDefence.EWR_55G6.id SAM_UNIT_TYPE = AirDefence.SAM_SA_10_S_300PS_SR_64H6E.id GARRISON_UNIT_TYPE = AirDefence.SAM_SA_19_Tunguska_2S6.id OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id MISSILE_SITE_UNIT_TYPE = MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M.id COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.SS_N_2_Silkworm.id # Multiple options for the required SAMs so campaign designers can more # accurately see the coverage of their IADS for the expected type. REQUIRED_LONG_RANGE_SAM_UNIT_TYPES = { AirDefence.SAM_Patriot_LN_M901.id, AirDefence.SAM_SA_10_S_300PS_LN_5P85C.id, AirDefence.SAM_SA_10_S_300PS_LN_5P85D.id, } REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES = { AirDefence.SAM_Hawk_LN_M192.id, AirDefence.SAM_SA_2_LN_SM_90.id, AirDefence.SAM_SA_3_S_125_LN_5P73.id, } BASE_DEFENSE_RADIUS = nm_to_meter(2) def __init__(self, miz: Path, theater: ConflictTheater) -> None: self.theater = theater self.mission = Mission() self.mission.load_file(str(miz)) self.control_point_id = itertools.count(1000) # 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) @staticmethod def control_point_from_airport(airport: Airport) -> ControlPoint: # The wiki says this is a legacy property and to just use regular. size = SIZE_REGULAR # The importance is taken from the periodicity of the airport's # warehouse divided by 10. 30 is the default, and out of range (valid # values are between 1.0 and 1.4). If it is used, pick the default # importance. if airport.periodicity == 30: importance = IMPORTANCE_MEDIUM else: importance = airport.periodicity / 10 cp = Airfield(airport, size, importance) cp.captured = airport.is_blue() # Use the unlimited aircraft option to determine if an airfield should # be owned by the player when the campaign is "inverted". cp.captured_invert = airport.unlimited_aircrafts return cp 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[FlyingGroup]: 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.blue.ship_group: if group.units[0].type == self.SHIP_UNIT_TYPE: yield group @property def ewrs(self) -> Iterator[VehicleGroup]: for group in self.blue.vehicle_group: if group.units[0].type == self.EWR_UNIT_TYPE: yield group @property def sams(self) -> Iterator[VehicleGroup]: for group in self.blue.vehicle_group: if group.units[0].type == self.SAM_UNIT_TYPE: yield group @property def garrisons(self) -> Iterator[VehicleGroup]: for group in self.blue.vehicle_group: if group.units[0].type == self.GARRISON_UNIT_TYPE: yield group @property def offshore_strike_targets(self) -> Iterator[StaticGroup]: for group in self.blue.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.blue.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.blue.vehicle_group: if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE: yield group @property def required_long_range_sams(self) -> Iterator[VehicleGroup]: for group in self.red.vehicle_group: if group.units[0].type in self.REQUIRED_LONG_RANGE_SAM_UNIT_TYPES: yield group @property def required_medium_range_sams(self) -> Iterator[VehicleGroup]: for group in self.red.vehicle_group: if group.units[ 0].type in self.REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES: yield group @cached_property def control_points(self) -> Dict[int, ControlPoint]: control_points = {} for airport in self.mission.terrain.airport_list(): if airport.is_blue() or airport.is_red(): control_point = self.control_point_from_airport(airport) control_points[control_point.id] = control_point for blue in (False, True): for group in self.off_map_spawns(blue): control_point = OffMapSpawn(next(self.control_point_id), str(group.name), group.position) control_point.captured = blue control_point.captured_invert = group.late_activation control_points[control_point.id] = control_point for group in self.carriers(blue): # TODO: Name the carrier. control_point = Carrier("carrier", group.position, next(self.control_point_id)) control_point.captured = blue control_point.captured_invert = group.late_activation control_points[control_point.id] = control_point for group in self.lhas(blue): # TODO: Name the LHA. control_point = Lha("lha", group.position, next(self.control_point_id)) control_point.captured = blue control_point.captured_invert = group.late_activation control_points[control_point.id] = control_point for group in self.fobs(blue): control_point = Fob(str(group.name), group.position, next(self.control_point_id)) control_point.captured = blue control_point.captured_invert = group.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 @cached_property def front_lines(self) -> Dict[str, ComplexFrontLine]: # Dict of front line ID to a front line. front_lines = {} 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. Intermediate waypoints # define the curve of the front line. 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}" ) # Snap the begin and end points to the control points. waypoints[0] = origin.position waypoints[-1] = destination.position front_line_id = f"{origin.id}|{destination.id}" front_lines[front_line_id] = ComplexFrontLine(origin, waypoints) self.control_points[origin.id].connect( self.control_points[destination.id]) self.control_points[destination.id].connect( self.control_points[origin.id]) return front_lines def objective_info(self, group: Group) -> Tuple[ControlPoint, int]: closest = self.theater.closest_control_point(group.position) distance = closest.position.distance_to_point(group.position) return closest, distance def add_preset_locations(self) -> None: for group in self.garrisons: closest, distance = self.objective_info(group) if distance < self.BASE_DEFENSE_RADIUS: closest.preset_locations.base_garrisons.append(group.position) else: logging.warning( f"Found garrison unit too far from base: {group.name}") for group in self.sams: closest, distance = self.objective_info(group) if distance < self.BASE_DEFENSE_RADIUS: closest.preset_locations.base_air_defense.append( group.position) else: closest.preset_locations.strike_locations.append( group.position) for group in self.ewrs: closest, distance = self.objective_info(group) closest.preset_locations.ewrs.append(group.position) for group in self.offshore_strike_targets: closest, distance = self.objective_info(group) closest.preset_locations.offshore_strike_locations.append( group.position) for group in self.ships: closest, distance = self.objective_info(group) closest.preset_locations.ships.append(group.position) for group in self.missile_sites: closest, distance = self.objective_info(group) closest.preset_locations.missile_sites.append(group.position) for group in self.coastal_defenses: closest, distance = self.objective_info(group) closest.preset_locations.coastal_defenses.append(group.position) for group in self.required_long_range_sams: closest, distance = self.objective_info(group) closest.preset_locations.required_long_range_sams.append( group.position) for group in self.required_medium_range_sams: closest, distance = self.objective_info(group) closest.preset_locations.required_medium_range_sams.append( group.position) def populate_theater(self) -> None: for control_point in self.control_points.values(): self.theater.add_controlpoint(control_point) self.add_preset_locations() self.theater.set_frontline_data(self.front_lines)
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_HELIPAD = "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 # 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._5p73_s_125_ln.id, } SHORT_RANGE_SAM_UNIT_TYPES = { AirDefence.M1097_Avenger.id, AirDefence.Rapier_fsa_launcher.id, AirDefence._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._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) -> None: self.theater = theater self.mission = Mission() with logged_duration("Loading miz"): self.mission.load_file(str(miz)) self.control_point_id = itertools.count(1000) # 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) @staticmethod def control_point_from_airport(airport: Airport) -> ControlPoint: # The wiki says this is a legacy property and to just use regular. size = SIZE_REGULAR # The importance is taken from the periodicity of the airport's # warehouse divided by 10. 30 is the default, and out of range (valid # values are between 1.0 and 1.4). If it is used, pick the default # importance. if airport.periodicity == 30: importance = IMPORTANCE_MEDIUM else: importance = airport.periodicity / 10 cp = Airfield(airport, size, importance) cp.captured = airport.is_blue() # Use the unlimited aircraft option to determine if an airfield should # be owned by the player when the campaign is "inverted". cp.captured_invert = airport.unlimited_aircrafts return cp 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 self.blue.static_group: if group.units[0].type == self.FARP_HELIPAD: 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(self.mission.triggers._zones) @cached_property def control_points(self) -> Dict[int, ControlPoint]: control_points = {} for airport in self.mission.terrain.airport_list(): if airport.is_blue() or airport.is_red(): control_point = self.control_point_from_airport(airport) control_points[control_point.id] = control_point for blue in (False, True): for group in self.off_map_spawns(blue): control_point = OffMapSpawn(next(self.control_point_id), str(group.name), group.position) control_point.captured = blue control_point.captured_invert = group.late_activation control_points[control_point.id] = control_point for ship in self.carriers(blue): # TODO: Name the carrier. control_point = Carrier("carrier", ship.position, next(self.control_point_id)) control_point.captured = blue control_point.captured_invert = ship.late_activation control_points[control_point.id] = control_point for ship in self.lhas(blue): # TODO: Name the LHA.db control_point = Lha("lha", ship.position, next(self.control_point_id)) control_point.captured = blue control_point.captured_invert = ship.late_activation control_points[control_point.id] = control_point for fob in self.fobs(blue): control_point = Fob(str(fob.name), fob.position, next(self.control_point_id)) control_point.captured = blue control_point.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 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 objective_info(self, near: Positioned) -> Tuple[ControlPoint, Distance]: closest = self.theater.closest_control_point(near.position) distance = meters(closest.position.distance_to_point(near.position)) return closest, distance def add_preset_locations(self) -> None: for static in self.offshore_strike_targets: closest, distance = self.objective_info(static) closest.preset_locations.offshore_strike_locations.append( PointWithHeading.from_point(static.position, static.units[0].heading)) for ship in self.ships: closest, distance = self.objective_info(ship) closest.preset_locations.ships.append( PointWithHeading.from_point(ship.position, ship.units[0].heading)) for group in self.missile_sites: closest, distance = self.objective_info(group) closest.preset_locations.missile_sites.append( PointWithHeading.from_point(group.position, group.units[0].heading)) for group in self.coastal_defenses: closest, distance = self.objective_info(group) closest.preset_locations.coastal_defenses.append( PointWithHeading.from_point(group.position, group.units[0].heading)) for group in self.long_range_sams: closest, distance = self.objective_info(group) closest.preset_locations.long_range_sams.append( PointWithHeading.from_point(group.position, group.units[0].heading)) for group in self.medium_range_sams: closest, distance = self.objective_info(group) closest.preset_locations.medium_range_sams.append( PointWithHeading.from_point(group.position, group.units[0].heading)) for group in self.short_range_sams: closest, distance = self.objective_info(group) closest.preset_locations.short_range_sams.append( PointWithHeading.from_point(group.position, group.units[0].heading)) for group in self.aaa: closest, distance = self.objective_info(group) closest.preset_locations.aaa.append( PointWithHeading.from_point(group.position, group.units[0].heading)) for group in self.ewrs: closest, distance = self.objective_info(group) closest.preset_locations.ewrs.append( PointWithHeading.from_point(group.position, group.units[0].heading)) for group in self.armor_groups: closest, distance = self.objective_info(group) closest.preset_locations.armor_groups.append( PointWithHeading.from_point(group.position, group.units[0].heading)) for static in self.helipads: closest, distance = self.objective_info(static) closest.helipads.append( PointWithHeading.from_point(static.position, static.units[0].heading)) for static in self.factories: closest, distance = self.objective_info(static) closest.preset_locations.factories.append( PointWithHeading.from_point(static.position, static.units[0].heading)) for static in self.ammunition_depots: closest, distance = self.objective_info(static) closest.preset_locations.ammunition_depots.append( PointWithHeading.from_point(static.position, static.units[0].heading)) for static in self.strike_targets: closest, distance = self.objective_info(static) closest.preset_locations.strike_locations.append( PointWithHeading.from_point(static.position, static.units[0].heading)) for scenery_group in self.scenery: closest, distance = self.objective_info(scenery_group) 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()