Merge pull request #204 from MetalStormGhost/pretense-generator

Pretense generator + additional options
This commit is contained in:
Raffson 2024-07-28 19:43:08 +02:00 committed by GitHub
commit ecb81ec32d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 23029 additions and 156 deletions

1
.gitignore vendored
View File

@ -22,5 +22,6 @@ env/
/logs/
/resources/logging.yaml
/resources/plugins/pretense/pretense_output.lua
*.psd

View File

@ -19,6 +19,7 @@ from .ocaaircraft import OcaAircraftFlightPlan
from .ocarunway import OcaRunwayFlightPlan
from .packagerefueling import PackageRefuelingFlightPlan
from .planningerror import PlanningError
from .pretensecargo import PretenseCargoFlightPlan
from .sead import SeadFlightPlan
from .seadsweep import SeadSweepFlightPlan
from .strike import StrikeFlightPlan
@ -61,6 +62,7 @@ class FlightPlanBuilderTypes:
FlightType.TRANSPORT: AirliftFlightPlan.builder_type(),
FlightType.FERRY: FerryFlightPlan.builder_type(),
FlightType.AIR_ASSAULT: AirAssaultFlightPlan.builder_type(),
FlightType.PRETENSE_CARGO: PretenseCargoFlightPlan.builder_type(),
FlightType.ARMED_RECON: ArmedReconFlightPlan.builder_type(),
}
try:

View File

@ -0,0 +1,112 @@
from __future__ import annotations
import random
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import datetime
from typing import TYPE_CHECKING, Type
from game.utils import feet
from .ferry import FerryLayout
from .ibuilder import IBuilder
from .planningerror import PlanningError
from .standard import StandardFlightPlan, StandardLayout
from .waypointbuilder import WaypointBuilder
from ...theater import OffMapSpawn
if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint
PRETENSE_CARGO_FLIGHT_DISTANCE = 100000
PRETENSE_CARGO_FLIGHT_HEADING_RANGE = 20
class PretenseCargoFlightPlan(StandardFlightPlan[FerryLayout]):
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@property
def tot_waypoint(self) -> FlightWaypoint:
return self.layout.arrival
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
# TOT planning isn't really useful for ferries. They're behind the front
# lines so no need to wait for escorts or for other missions to complete.
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
return None
@property
def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> datetime:
return self.package.time_over_target
class Builder(IBuilder[PretenseCargoFlightPlan, FerryLayout]):
def layout(self) -> FerryLayout:
# Find the spawn location for off-map transport planes
distance_to_flot = 0.0
heading_from_flot = 0.0
offmap_transport_cp_id = self.flight.departure.id
for front_line_cp in self.coalition.game.theater.controlpoints:
if isinstance(front_line_cp, OffMapSpawn):
continue
for front_line in self.coalition.game.theater.conflicts():
if front_line_cp.captured == self.flight.coalition.player:
if (
front_line_cp.position.distance_to_point(front_line.position)
> distance_to_flot
):
distance_to_flot = front_line_cp.position.distance_to_point(
front_line.position
)
heading_from_flot = front_line.position.heading_between_point(
front_line_cp.position
)
offmap_transport_cp_id = front_line_cp.id
offmap_transport_cp = self.coalition.game.theater.find_control_point_by_id(
offmap_transport_cp_id
)
offmap_heading = random.randrange(
int(heading_from_flot - PRETENSE_CARGO_FLIGHT_HEADING_RANGE),
int(heading_from_flot + PRETENSE_CARGO_FLIGHT_HEADING_RANGE),
)
offmap_transport_spawn = offmap_transport_cp.position.point_from_heading(
offmap_heading, PRETENSE_CARGO_FLIGHT_DISTANCE
)
altitude_is_agl = self.flight.is_helo
altitude = (
feet(self.coalition.game.settings.heli_cruise_alt_agl)
if altitude_is_agl
else self.flight.unit_type.preferred_patrol_altitude
)
builder = WaypointBuilder(self.flight)
ferry_layout = FerryLayout(
departure=builder.join(offmap_transport_spawn),
nav_to=builder.nav_path(
offmap_transport_spawn,
self.flight.arrival.position,
altitude,
altitude_is_agl,
),
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
nav_from=[],
custom_waypoints=list(),
)
ferry_layout.departure = builder.join(offmap_transport_spawn)
ferry_layout.nav_to.append(builder.join(offmap_transport_spawn))
ferry_layout.nav_from.append(builder.join(offmap_transport_spawn))
return ferry_layout
def build(self, dump_debug_info: bool = False) -> PretenseCargoFlightPlan:
return PretenseCargoFlightPlan(self.flight, self.layout())

View File

@ -58,6 +58,7 @@ class FlightType(Enum):
FERRY = "Ferry"
AIR_ASSAULT = "Air Assault"
SEAD_SWEEP = "SEAD Sweep" # Reintroduce legacy "engage-whatever-you-can-find" SEAD
PRETENSE_CARGO = "Cargo Transport" # For Pretense campaign AI cargo planes
ARMED_RECON = "Armed Recon"
def __str__(self) -> str:
@ -124,5 +125,6 @@ class FlightType(Enum):
FlightType.SWEEP: AirEntity.FIGHTER,
FlightType.TARCAP: AirEntity.FIGHTER,
FlightType.TRANSPORT: AirEntity.UTILITY,
FlightType.PRETENSE_CARGO: AirEntity.UTILITY,
FlightType.AIR_ASSAULT: AirEntity.ROTARY_WING,
}.get(self, AirEntity.UNSPECIFIED)

View File

@ -23,6 +23,8 @@ class SquadronDefGenerator:
def generate_for_task(
self, task: FlightType, control_point: ControlPoint
) -> Optional[SquadronDef]:
settings = control_point.coalition.game.settings
squadron_random_chance = settings.squadron_random_chance
aircraft_choice: Optional[AircraftType] = None
for aircraft in AircraftType.priority_list_for_task(task):
if aircraft not in self.faction.all_aircrafts:
@ -30,9 +32,9 @@ class SquadronDefGenerator:
if not control_point.can_operate(aircraft):
continue
aircraft_choice = aircraft
# 50/50 chance to keep looking for an aircraft that isn't as far up the
# squadron_random_chance percent chance to keep looking for an aircraft that isn't as far up the
# priority list to maintain some unit variety.
if random.choice([True, False]):
if squadron_random_chance >= random.randint(1, 100):
break
if aircraft_choice is None:

View File

@ -22,6 +22,7 @@ from game.models.game_stats import GameStats
from game.plugins import LuaPluginManager
from game.utils import Distance
from . import naming, persistency
from .ato import Flight
from .ato.flighttype import FlightType
from .campaignloader import CampaignAirWingConfig
from .coalition import Coalition
@ -148,6 +149,16 @@ class Game:
self.blue.configure_default_air_wing(air_wing_config)
self.red.configure_default_air_wing(air_wing_config)
# Side, control point, mission type
self.pretense_ground_supply: dict[int, dict[str, List[str]]] = {1: {}, 2: {}}
self.pretense_ground_assault: dict[int, dict[str, List[str]]] = {1: {}, 2: {}}
self.pretense_air: dict[int, dict[str, dict[FlightType, List[str]]]] = {
1: {},
2: {},
}
self.pretense_air_groups: dict[str, Flight] = {}
self.pretense_carrier_zones: List[str] = []
self.on_load(game_still_initializing=True)
def __setstate__(self, state: dict[str, Any]) -> None:

View File

@ -1,6 +1,7 @@
from __future__ import annotations
import logging
import random
from datetime import datetime
from typing import Any, Optional, TYPE_CHECKING
@ -19,7 +20,12 @@ from game.data.weapons import Pylon, WeaponType
from game.missiongenerator.logisticsgenerator import LogisticsGenerator
from game.missiongenerator.missiondata import MissionData, AwacsInfo, TankerInfo
from game.radio.radios import RadioFrequency, RadioRegistry
from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage
from game.radio.tacan import (
TacanBand,
TacanRegistry,
TacanUsage,
OutOfTacanChannelsError,
)
from game.runways import RunwayData
from game.squadrons import Pilot
from .aircraftbehavior import AircraftBehavior
@ -210,9 +216,12 @@ class FlightGroupConfigurator:
) or isinstance(self.flight.flight_plan, PackageRefuelingFlightPlan):
tacan = self.flight.tacan
if tacan is None and self.flight.squadron.aircraft.dcs_unit_type.tacan:
tacan = self.tacan_registry.alloc_for_band(
TacanBand.Y, TacanUsage.AirToAir
)
try:
tacan = self.tacan_registry.alloc_for_band(
TacanBand.Y, TacanUsage.AirToAir
)
except OutOfTacanChannelsError:
tacan = random.choice(list(self.tacan_registry.allocated_channels))
else:
tacan = self.flight.tacan
self.mission_data.tankers.append(

View File

@ -15,6 +15,7 @@ from dcs.planes import (
C_101CC,
Su_33,
MiG_15bis,
M_2000C,
)
from dcs.point import PointAction
from dcs.ships import KUZNECOW
@ -36,7 +37,7 @@ from game.missiongenerator.missiondata import MissionData
from game.naming import namegen
from game.theater import Airfield, ControlPoint, Fob, NavalControlPoint, OffMapSpawn
from game.utils import feet, meters
from pydcs_extensions import A_4E_C
from pydcs_extensions import A_4E_C, VSN_F4B, VSN_F4C
WARM_START_HELI_ALT = meters(500)
WARM_START_ALTITUDE = meters(3000)
@ -496,7 +497,7 @@ class FlightGroupSpawner:
) -> Optional[FlyingGroup[Any]]:
is_airbase = False
is_roadbase = False
ground_spawn = None
ground_spawn: Optional[Tuple[StaticGroup, Point]] = None
if not is_large and len(self.ground_spawns_roadbase[cp]) > 0:
ground_spawn = self.ground_spawns_roadbase[cp].pop()
@ -519,6 +520,8 @@ class FlightGroupSpawner:
group.points[0].type = "TakeOffGround"
group.units[0].heading = ground_spawn[0].units[0].heading
self._remove_invisible_farps_if_requested(cp, ground_spawn[0], group)
# Hot start aircraft which require ground power to start, when ground power
# trucks have been disabled for performance reasons
ground_power_available = (
@ -529,10 +532,31 @@ class FlightGroupSpawner:
and self.flight.coalition.game.settings.ground_start_ground_power_trucks_roadbase
)
if self.start_type is not StartType.COLD or (
not ground_power_available
and self.flight.unit_type.dcs_unit_type
in [A_4E_C, F_5E_3, F_86F_Sabre, MiG_15bis, F_14A_135_GR, F_14B, C_101CC]
# Also hot start aircraft which require ground crew support (ground air or chock removal)
# which might not be available at roadbases
if (
self.start_type is not StartType.COLD
or (
not ground_power_available
and self.flight.unit_type.dcs_unit_type
in [
A_4E_C,
F_86F_Sabre,
MiG_15bis,
F_14A_135_GR,
F_14B,
C_101CC,
]
)
or (
self.flight.unit_type.dcs_unit_type
in [
F_5E_3,
M_2000C,
VSN_F4B,
VSN_F4C,
]
)
):
group.points[0].action = PointAction.FromGroundAreaHot
group.points[0].type = "TakeOffGroundHot"
@ -556,12 +580,33 @@ class FlightGroupSpawner:
ground_spawn[0].x, ground_spawn[0].y, terrain=terrain
)
group.units[1 + i].heading = ground_spawn[0].units[0].heading
self._remove_invisible_farps_if_requested(cp, ground_spawn[0])
except IndexError as ex:
raise RuntimeError(
f"Not enough ground spawn slots available at {cp}"
raise NoParkingSlotError(
f"Not enough STOL slots available at {cp}"
) from ex
return group
def _remove_invisible_farps_if_requested(
self,
cp: ControlPoint,
ground_spawn: StaticGroup,
group: Optional[FlyingGroup[Any]] = None,
) -> None:
if (
cp.coalition.game.settings.ground_start_airbase_statics_farps_remove
and isinstance(cp, Airfield)
):
# Remove invisible FARPs from airfields because they are unnecessary
neutral_country = self.mission.country(
cp.coalition.game.neutral_country.name
)
neutral_country.remove_static_group(ground_spawn)
if group:
group.points[0].link_unit = None
group.points[0].helipad_id = None
def dcs_start_type(self) -> DcsStartType:
if self.start_type is StartType.RUNWAY:
return DcsStartType.Runway

View File

@ -52,6 +52,8 @@ class CarrierInfo(UnitInfo):
"""Carrier information."""
tacan: TacanChannel
icls_channel: int | None
link4_freq: RadioFrequency | None
@dataclass

View File

@ -67,6 +67,7 @@ from game.theater import (
TheaterGroundObject,
TheaterUnit,
NavalControlPoint,
Airfield,
)
from game.theater.theatergroundobject import (
CarrierGroundObject,
@ -297,11 +298,9 @@ class GroundObjectGenerator:
# All alive Ships
ship_units.append(unit)
if vehicle_units:
vg = self.create_vehicle_group(group.group_name, vehicle_units)
vg.hidden_on_mfd = self.ground_object.hide_on_mfd
self.create_vehicle_group(group.group_name, vehicle_units)
if ship_units:
sg = self.create_ship_group(group.group_name, ship_units)
sg.hidden_on_mfd = self.ground_object.hide_on_mfd
self.create_ship_group(group.group_name, ship_units)
def create_vehicle_group(
self, group_name: str, units: list[TheaterUnit]
@ -333,6 +332,7 @@ class GroundObjectGenerator:
self._register_theater_unit(unit, vehicle_group.units[-1])
if vehicle_group is None:
raise RuntimeError(f"Error creating VehicleGroup for {group_name}")
vehicle_group.hidden_on_mfd = self.ground_object.hide_on_mfd
return vehicle_group
def create_ship_group(
@ -369,6 +369,7 @@ class GroundObjectGenerator:
self._register_theater_unit(unit, ship_group.units[-1])
if ship_group is None:
raise RuntimeError(f"Error creating ShipGroup for {group_name}")
ship_group.hidden_on_mfd = self.ground_object.hide_on_mfd
return ship_group
def create_static_group(self, unit: TheaterUnit) -> None:
@ -645,6 +646,8 @@ class GenericCarrierGenerator(GroundObjectGenerator):
callsign=tacan_callsign,
freq=atc,
tacan=tacan,
icls_channel=icls,
link4_freq=link4,
blue=self.control_point.captured,
)
)
@ -844,6 +847,20 @@ class HelipadGenerator:
else:
self.helipads.append(sg)
if self.game.position_culled(helipad):
cull_farp_statics = True
if self.cp.coalition.player:
for package in self.cp.coalition.ato.packages:
for flight in package.flights:
if flight.squadron.location == self.cp:
cull_farp_statics = False
break
elif flight.divert and flight.divert == self.cp:
cull_farp_statics = False
break
else:
cull_farp_statics = False
warehouse = Airport(
pad.position,
self.m.terrain,
@ -852,30 +869,31 @@ class HelipadGenerator:
# configure dynamic spawn + hot start of DS, plus dynamic cargo?
self.m.warehouses.warehouses[pad.id] = warehouse
# Generate a FARP Ammo and Fuel stack for each pad
self.m.static_group(
country=country,
name=(name + "_fuel"),
_type=Fortification.FARP_Fuel_Depot,
position=pad.position.point_from_heading(helipad.heading.degrees, 35),
heading=pad.heading + 180,
)
self.m.static_group(
country=country,
name=(name + "_ammo"),
_type=Fortification.FARP_Ammo_Dump_Coating,
position=pad.position.point_from_heading(
helipad.heading.degrees, 35
).point_from_heading(helipad.heading.degrees + 90, 10),
heading=pad.heading + 90,
)
self.m.static_group(
country=country,
name=(name + "_ws"),
_type=Fortification.Windsock,
position=helipad.point_from_heading(helipad.heading.degrees + 45, 35),
heading=pad.heading,
)
if not cull_farp_statics:
# Generate a FARP Ammo and Fuel stack for each pad
self.m.static_group(
country=country,
name=(name + "_fuel"),
_type=Fortification.FARP_Fuel_Depot,
position=pad.position.point_from_heading(helipad.heading.degrees, 35),
heading=pad.heading + 180,
)
self.m.static_group(
country=country,
name=(name + "_ammo"),
_type=Fortification.FARP_Ammo_Dump_Coating,
position=pad.position.point_from_heading(
helipad.heading.degrees, 35
).point_from_heading(helipad.heading.degrees + 90, 10),
heading=pad.heading + 90,
)
self.m.static_group(
country=country,
name=(name + "_ws"),
_type=Fortification.Windsock,
position=helipad.point_from_heading(helipad.heading.degrees + 45, 35),
heading=pad.heading,
)
def append_helipad(
self,
@ -952,61 +970,88 @@ class GroundSpawnRoadbaseGenerator:
country.id
)
# Generate ammo truck/farp and fuel truck/stack for each pad
if self.game.settings.ground_start_trucks_roadbase:
self.m.vehicle_group(
country=country,
name=(name + "_fuel"),
_type=tanker_type,
position=pad.position.point_from_heading(
ground_spawn[0].heading.degrees + 90, 35
),
group_size=1,
heading=pad.heading + 315,
move_formation=PointAction.OffRoad,
)
self.m.vehicle_group(
country=country,
name=(name + "_ammo"),
_type=ammo_truck_type,
position=pad.position.point_from_heading(
ground_spawn[0].heading.degrees + 90, 35
).point_from_heading(ground_spawn[0].heading.degrees + 180, 10),
group_size=1,
heading=pad.heading + 315,
move_formation=PointAction.OffRoad,
)
if self.game.settings.ground_start_airbase_statics_farps_remove and isinstance(
self.cp, Airfield
):
cull_farp_statics = True
elif self.game.position_culled(ground_spawn[0]):
cull_farp_statics = True
if self.cp.coalition.player:
for package in self.cp.coalition.ato.packages:
for flight in package.flights:
if flight.squadron.location == self.cp:
cull_farp_statics = False
break
elif flight.divert and flight.divert == self.cp:
cull_farp_statics = False
break
else:
self.m.static_group(
country=country,
name=(name + "_fuel"),
_type=Fortification.FARP_Fuel_Depot,
position=pad.position.point_from_heading(
ground_spawn[0].heading.degrees + 90, 35
),
heading=pad.heading + 270,
)
self.m.static_group(
country=country,
name=(name + "_ammo"),
_type=Fortification.FARP_Ammo_Dump_Coating,
position=pad.position.point_from_heading(
ground_spawn[0].heading.degrees + 90, 35
).point_from_heading(ground_spawn[0].heading.degrees + 180, 10),
heading=pad.heading + 180,
)
if self.game.settings.ground_start_ground_power_trucks_roadbase:
self.m.vehicle_group(
country=country,
name=(name + "_power"),
_type=power_truck_type,
position=pad.position.point_from_heading(
ground_spawn[0].heading.degrees + 90, 35
).point_from_heading(ground_spawn[0].heading.degrees + 180, 20),
group_size=1,
heading=pad.heading + 315,
move_formation=PointAction.OffRoad,
)
cull_farp_statics = False
warehouse = Airport(
pad.position,
self.m.terrain,
).dict()
warehouse["coalition"] = "blue" if self.cp.coalition.player else "red"
# configure dynamic spawn + hot start of DS, plus dynamic cargo?
self.m.warehouses.warehouses[pad.id] = warehouse
if not cull_farp_statics:
# Generate ammo truck/farp and fuel truck/stack for each pad
if self.game.settings.ground_start_trucks_roadbase:
self.m.vehicle_group(
country=country,
name=(name + "_fuel"),
_type=tanker_type,
position=pad.position.point_from_heading(
ground_spawn[0].heading.degrees + 90, 35
),
group_size=1,
heading=pad.heading + 315,
move_formation=PointAction.OffRoad,
)
self.m.vehicle_group(
country=country,
name=(name + "_ammo"),
_type=ammo_truck_type,
position=pad.position.point_from_heading(
ground_spawn[0].heading.degrees + 90, 35
).point_from_heading(ground_spawn[0].heading.degrees + 180, 10),
group_size=1,
heading=pad.heading + 315,
move_formation=PointAction.OffRoad,
)
else:
self.m.static_group(
country=country,
name=(name + "_fuel"),
_type=Fortification.FARP_Fuel_Depot,
position=pad.position.point_from_heading(
ground_spawn[0].heading.degrees + 90, 35
),
heading=pad.heading + 270,
)
self.m.static_group(
country=country,
name=(name + "_ammo"),
_type=Fortification.FARP_Ammo_Dump_Coating,
position=pad.position.point_from_heading(
ground_spawn[0].heading.degrees + 90, 35
).point_from_heading(ground_spawn[0].heading.degrees + 180, 10),
heading=pad.heading + 180,
)
if self.game.settings.ground_start_ground_power_trucks_roadbase:
self.m.vehicle_group(
country=country,
name=(name + "_power"),
_type=power_truck_type,
position=pad.position.point_from_heading(
ground_spawn[0].heading.degrees + 90, 35
).point_from_heading(ground_spawn[0].heading.degrees + 180, 20),
group_size=1,
heading=pad.heading + 315,
move_formation=PointAction.OffRoad,
)
def generate(self) -> None:
try:
@ -1069,6 +1114,14 @@ class GroundSpawnLargeGenerator:
country.id
)
warehouse = Airport(
pad.position,
self.m.terrain,
).dict()
warehouse["coalition"] = "blue" if self.cp.coalition.player else "red"
# configure dynamic spawn + hot start of DS, plus dynamic cargo?
self.m.warehouses.warehouses[pad.id] = warehouse
# Generate a FARP Ammo and Fuel stack for each pad
if self.game.settings.ground_start_trucks:
self.m.vehicle_group(
@ -1186,61 +1239,88 @@ class GroundSpawnGenerator:
country.id
)
# Generate a FARP Ammo and Fuel stack for each pad
if self.game.settings.ground_start_trucks:
self.m.vehicle_group(
country=country,
name=(name + "_fuel"),
_type=tanker_type,
position=pad.position.point_from_heading(
vtol_pad[0].heading.degrees - 175, 35
),
group_size=1,
heading=pad.heading + 45,
move_formation=PointAction.OffRoad,
)
self.m.vehicle_group(
country=country,
name=(name + "_ammo"),
_type=ammo_truck_type,
position=pad.position.point_from_heading(
vtol_pad[0].heading.degrees - 185, 35
),
group_size=1,
heading=pad.heading + 45,
move_formation=PointAction.OffRoad,
)
if self.game.settings.ground_start_airbase_statics_farps_remove and isinstance(
self.cp, Airfield
):
cull_farp_statics = True
elif self.game.position_culled(vtol_pad[0]):
cull_farp_statics = True
if self.cp.coalition.player:
for package in self.cp.coalition.ato.packages:
for flight in package.flights:
if flight.squadron.location == self.cp:
cull_farp_statics = False
break
elif flight.divert and flight.divert == self.cp:
cull_farp_statics = False
break
else:
self.m.static_group(
country=country,
name=(name + "_fuel"),
_type=Fortification.FARP_Fuel_Depot,
position=pad.position.point_from_heading(
vtol_pad[0].heading.degrees - 180, 45
),
heading=pad.heading,
)
self.m.static_group(
country=country,
name=(name + "_ammo"),
_type=Fortification.FARP_Ammo_Dump_Coating,
position=pad.position.point_from_heading(
vtol_pad[0].heading.degrees - 180, 35
),
heading=pad.heading + 270,
)
if self.game.settings.ground_start_ground_power_trucks:
self.m.vehicle_group(
country=country,
name=(name + "_power"),
_type=power_truck_type,
position=pad.position.point_from_heading(
vtol_pad[0].heading.degrees - 185, 35
),
group_size=1,
heading=pad.heading + 45,
move_formation=PointAction.OffRoad,
)
cull_farp_statics = False
if not cull_farp_statics:
warehouse = Airport(
pad.position,
self.m.terrain,
).dict()
warehouse["coalition"] = "blue" if self.cp.coalition.player else "red"
# configure dynamic spawn + hot start of DS, plus dynamic cargo?
self.m.warehouses.warehouses[pad.id] = warehouse
# Generate a FARP Ammo and Fuel stack for each pad
if self.game.settings.ground_start_trucks:
self.m.vehicle_group(
country=country,
name=(name + "_fuel"),
_type=tanker_type,
position=pad.position.point_from_heading(
vtol_pad[0].heading.degrees - 175, 35
),
group_size=1,
heading=pad.heading + 45,
move_formation=PointAction.OffRoad,
)
self.m.vehicle_group(
country=country,
name=(name + "_ammo"),
_type=ammo_truck_type,
position=pad.position.point_from_heading(
vtol_pad[0].heading.degrees - 185, 35
),
group_size=1,
heading=pad.heading + 45,
move_formation=PointAction.OffRoad,
)
else:
self.m.static_group(
country=country,
name=(name + "_fuel"),
_type=Fortification.FARP_Fuel_Depot,
position=pad.position.point_from_heading(
vtol_pad[0].heading.degrees - 180, 45
),
heading=pad.heading,
)
self.m.static_group(
country=country,
name=(name + "_ammo"),
_type=Fortification.FARP_Ammo_Dump_Coating,
position=pad.position.point_from_heading(
vtol_pad[0].heading.degrees - 180, 35
),
heading=pad.heading + 270,
)
if self.game.settings.ground_start_ground_power_trucks:
self.m.vehicle_group(
country=country,
name=(name + "_power"),
_type=power_truck_type,
position=pad.position.point_from_heading(
vtol_pad[0].heading.degrees - 185, 35
),
group_size=1,
heading=pad.heading + 45,
move_formation=PointAction.OffRoad,
)
def generate(self) -> None:
try:

View File

@ -154,6 +154,10 @@ def save_dir() -> Path:
return base_path() / "Retribution" / "Saves"
def pre_pretense_backups_dir() -> Path:
return save_dir() / "PrePretenseBackups"
def server_port() -> int:
global _server_port
return _server_port

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,155 @@
from __future__ import annotations
from datetime import datetime
from typing import Any, Optional, TYPE_CHECKING
from dcs import Mission, Point
from dcs.flyingunit import FlyingUnit
from dcs.unitgroup import FlyingGroup
from game.ato import Flight, FlightType
from game.ato.flightmember import FlightMember
from game.data.weapons import Pylon
from game.lasercodes.lasercoderegistry import LaserCodeRegistry
from game.missiongenerator.aircraft.aircraftbehavior import AircraftBehavior
from game.missiongenerator.aircraft.aircraftpainter import AircraftPainter
from game.missiongenerator.aircraft.bingoestimator import BingoEstimator
from game.missiongenerator.aircraft.flightdata import FlightData
from game.missiongenerator.aircraft.flightgroupconfigurator import (
FlightGroupConfigurator,
)
from game.missiongenerator.aircraft.waypoints import WaypointGenerator
from game.missiongenerator.missiondata import MissionData
from game.radio.radios import RadioRegistry
from game.radio.tacan import (
TacanRegistry,
)
from game.runways import RunwayData
if TYPE_CHECKING:
from game import Game
class PretenseFlightGroupConfigurator(FlightGroupConfigurator):
def __init__(
self,
flight: Flight,
group: FlyingGroup[Any],
game: Game,
mission: Mission,
time: datetime,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
mission_data: MissionData,
dynamic_runways: dict[str, RunwayData],
use_client: bool,
) -> None:
super().__init__(
flight,
group,
game,
mission,
time,
radio_registry,
tacan_registry,
mission_data,
dynamic_runways,
use_client,
)
self.flight = flight
self.group = group
self.game = game
self.mission = mission
self.time = time
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
self.mission_data = mission_data
self.dynamic_runways = dynamic_runways
self.use_client = use_client
def configure(self) -> FlightData:
AircraftBehavior(self.flight.flight_type).apply_to(self.flight, self.group)
AircraftPainter(self.flight, self.group).apply_livery()
self.setup_props()
self.setup_payloads()
self.setup_fuel()
flight_channel = self.setup_radios()
laser_codes: list[Optional[int]] = []
for unit, pilot in zip(self.group.units, self.flight.roster.members):
self.configure_flight_member(unit, pilot, laser_codes)
divert = None
if self.flight.divert is not None:
divert = self.flight.divert.active_runway(
self.game.theater, self.game.conditions, self.dynamic_runways
)
mission_start_time, waypoints = WaypointGenerator(
self.flight,
self.group,
self.mission,
self.time,
self.game.settings,
self.mission_data,
).create_waypoints()
divert_position: Point | None = None
if self.flight.divert is not None:
divert_position = self.flight.divert.position
bingo_estimator = BingoEstimator(
self.flight.unit_type.fuel_consumption,
self.flight.arrival.position,
divert_position,
self.flight.flight_plan.waypoints,
)
self.group.uncontrolled = False
return FlightData(
package=self.flight.package,
aircraft_type=self.flight.unit_type,
squadron=self.flight.squadron,
flight_type=self.flight.flight_type,
units=self.group.units,
size=len(self.group.units),
friendly=self.flight.departure.captured,
departure_delay=mission_start_time,
departure=self.flight.departure.active_runway(
self.game.theater, self.game.conditions, self.dynamic_runways
),
arrival=self.flight.arrival.active_runway(
self.game.theater, self.game.conditions, self.dynamic_runways
),
divert=divert,
waypoints=waypoints,
intra_flight_channel=flight_channel,
bingo_fuel=bingo_estimator.estimate_bingo(),
joker_fuel=bingo_estimator.estimate_joker(),
custom_name=self.flight.custom_name,
laser_codes=laser_codes,
)
def setup_payloads(self) -> None:
for unit, member in zip(self.group.units, self.flight.iter_members()):
self.setup_payload(unit, member)
def setup_payload(self, unit: FlyingUnit, member: FlightMember) -> None:
unit.pylons.clear()
loadout = member.loadout
if self.flight.flight_type == FlightType.SEAD:
loadout = member.loadout.default_for_task_and_aircraft(
FlightType.SEAD_SWEEP, self.flight.unit_type.dcs_unit_type
)
if self.game.settings.restrict_weapons_by_date:
loadout = loadout.degrade_for_date(self.flight.unit_type, self.game.date)
for pylon_number, weapon in loadout.pylons.items():
if weapon is None:
continue
pylon = Pylon.for_aircraft(self.flight.unit_type, pylon_number)
pylon.equip(unit, weapon)

View File

@ -0,0 +1,261 @@
import logging
import random
from typing import Any, Tuple
from dcs import Mission
from dcs.country import Country
from dcs.mapping import Vector2, Point
from dcs.terrain import NoParkingSlotError, TheChannel, Falklands
from dcs.terrain.falklands.airports import San_Carlos_FOB, Goose_Green, Gull_Point
from dcs.terrain.thechannel.airports import Manston
from dcs.unitgroup import (
FlyingGroup,
ShipGroup,
StaticGroup,
)
from game.ato import Flight
from game.ato.flightstate import InFlight
from game.ato.starttype import StartType
from game.missiongenerator.aircraft.flightgroupspawner import (
FlightGroupSpawner,
MINIMUM_MID_MISSION_SPAWN_ALTITUDE_AGL,
MINIMUM_MID_MISSION_SPAWN_ALTITUDE_MSL,
STACK_SEPARATION,
)
from game.missiongenerator.missiondata import MissionData
from game.naming import NameGenerator
from game.theater import Airfield, ControlPoint, Fob, NavalControlPoint
class PretenseNameGenerator(NameGenerator):
@classmethod
def next_pretense_aircraft_name(cls, cp: ControlPoint, flight: Flight) -> str:
cls.aircraft_number += 1
cp_name_trimmed = cls.pretense_trimmed_cp_name(cp.name)
return "{}-{}-{}".format(
cp_name_trimmed, str(flight.flight_type).lower(), cls.aircraft_number
)
@classmethod
def pretense_trimmed_cp_name(cls, cp_name: str) -> str:
cp_name_alnum = "".join([i for i in cp_name.lower() if i.isalnum()])
cp_name_trimmed = cp_name_alnum.lstrip("1 2 3 4 5 6 7 8 9 0")
cp_name_trimmed = cp_name_trimmed.replace("ä", "a")
cp_name_trimmed = cp_name_trimmed.replace("ö", "o")
cp_name_trimmed = cp_name_trimmed.replace("ø", "o")
return cp_name_trimmed
namegen = PretenseNameGenerator
# Air-start AI aircraft which are faster than this on WWII terrains
# 1000 km/h is just above the max speed of the Harrier and Su-25,
# so they will still start normally from grass and dirt strips
WW2_TERRAIN_SUPERSONIC_AI_AIRSTART_SPEED = 1000
class PretenseFlightGroupSpawner(FlightGroupSpawner):
def __init__(
self,
flight: Flight,
country: Country,
mission: Mission,
helipads: dict[ControlPoint, list[StaticGroup]],
ground_spawns_roadbase: dict[ControlPoint, list[Tuple[StaticGroup, Point]]],
ground_spawns_large: dict[ControlPoint, list[Tuple[StaticGroup, Point]]],
ground_spawns: dict[ControlPoint, list[Tuple[StaticGroup, Point]]],
mission_data: MissionData,
) -> None:
super().__init__(
flight,
country,
mission,
helipads,
ground_spawns_roadbase,
ground_spawns_large,
ground_spawns,
mission_data,
)
self.flight = flight
self.country = country
self.mission = mission
self.helipads = helipads
self.ground_spawns_roadbase = ground_spawns_roadbase
self.ground_spawns = ground_spawns
self.mission_data = mission_data
def insert_into_pretense(self, name: str) -> None:
cp = self.flight.departure
is_player = True
cp_side = (
2
if self.flight.coalition
== self.flight.coalition.game.coalition_for(is_player)
else 1
)
cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(cp.name)
if self.flight.client_count == 0:
self.flight.coalition.game.pretense_air[cp_side][cp_name_trimmed][
self.flight.flight_type
].append(name)
try:
self.flight.coalition.game.pretense_air_groups[name] = self.flight
except AttributeError:
self.flight.coalition.game.pretense_air_groups = {}
self.flight.coalition.game.pretense_air_groups[name] = self.flight
def generate_flight_at_departure(self) -> FlyingGroup[Any]:
cp = self.flight.departure
name = namegen.next_pretense_aircraft_name(cp, self.flight)
try:
if self.start_type is StartType.IN_FLIGHT:
self.insert_into_pretense(name)
group = self._generate_over_departure(name, cp)
return group
elif isinstance(cp, NavalControlPoint):
group_name = cp.get_carrier_group_name()
carrier_group = self.mission.find_group(group_name)
if not isinstance(carrier_group, ShipGroup):
raise RuntimeError(
f"Carrier group {carrier_group} is a "
f"{carrier_group.__class__.__name__}, expected a ShipGroup"
)
self.insert_into_pretense(name)
return self._generate_at_group(name, carrier_group)
elif isinstance(cp, Fob):
is_heli = self.flight.squadron.aircraft.helicopter
is_vtol = not is_heli and self.flight.squadron.aircraft.lha_capable
if not is_heli and not is_vtol and not cp.has_ground_spawns:
raise RuntimeError(
f"Cannot spawn fixed-wing aircraft at {cp} because of insufficient ground spawn slots."
)
pilot_count = len(self.flight.roster.members)
if (
not is_heli
and self.flight.roster.player_count != pilot_count
and not self.flight.coalition.game.settings.ground_start_ai_planes
):
raise RuntimeError(
f"Fixed-wing aircraft at {cp} must be piloted by humans exclusively because"
f' the "AI fixed-wing aircraft can use roadbases / bases with only ground'
f' spawns" setting is currently disabled.'
)
if cp.has_helipads and (is_heli or is_vtol):
self.insert_into_pretense(name)
pad_group = self._generate_at_cp_helipad(name, cp)
if pad_group is not None:
return pad_group
if cp.has_ground_spawns and (self.flight.client_count > 0 or is_heli):
self.insert_into_pretense(name)
pad_group = self._generate_at_cp_ground_spawn(name, cp)
if pad_group is not None:
return pad_group
raise NoParkingSlotError
elif isinstance(cp, Airfield):
is_heli = self.flight.squadron.aircraft.helicopter
is_vtol = not is_heli and self.flight.squadron.aircraft.lha_capable
if cp.has_helipads and is_heli:
self.insert_into_pretense(name)
pad_group = self._generate_at_cp_helipad(name, cp)
if pad_group is not None:
return pad_group
# Air-start supersonic AI aircraft if the campaign is being flown in a WWII terrain
# This will improve these terrains' use in cold war campaigns
if isinstance(cp.theater.terrain, TheChannel) and not isinstance(
cp.dcs_airport, Manston
):
if (
self.flight.client_count == 0
and self.flight.squadron.aircraft.max_speed.speed_in_kph
> WW2_TERRAIN_SUPERSONIC_AI_AIRSTART_SPEED
):
self.insert_into_pretense(name)
return self._generate_over_departure(name, cp)
# Air-start AI fixed wing (non-VTOL) aircraft if the campaign is being flown in the South Atlantic terrain and
# the airfield is one of the Harrier-only ones in East Falklands.
# This will help avoid AI aircraft from smashing into the end of the runway and exploding.
if isinstance(cp.theater.terrain, Falklands) and (
isinstance(cp.dcs_airport, San_Carlos_FOB)
or isinstance(cp.dcs_airport, Goose_Green)
or isinstance(cp.dcs_airport, Gull_Point)
):
if self.flight.client_count == 0 and is_vtol:
self.insert_into_pretense(name)
return self._generate_over_departure(name, cp)
if (
cp.has_ground_spawns
and len(self.ground_spawns[cp])
+ len(self.ground_spawns_roadbase[cp])
>= self.flight.count
and (self.flight.client_count > 0 or is_heli)
):
self.insert_into_pretense(name)
pad_group = self._generate_at_cp_ground_spawn(name, cp)
if pad_group is not None:
return pad_group
self.insert_into_pretense(name)
return self._generate_at_airfield(name, cp)
else:
raise NotImplementedError(
f"Aircraft spawn behavior not implemented for {cp} ({cp.__class__})"
)
except NoParkingSlotError:
# Generated when there is no place on Runway or on Parking Slots
if self.flight.client_count > 0:
# Don't generate player airstarts
logging.warning(
"No room on runway or parking slots. Not generating a player air-start."
)
raise NoParkingSlotError
else:
logging.warning(
"No room on runway or parking slots. Starting from the air."
)
self.flight.start_type = StartType.IN_FLIGHT
self.insert_into_pretense(name)
group = self._generate_over_departure(name, cp)
return group
def generate_mid_mission(self) -> FlyingGroup[Any]:
assert isinstance(self.flight.state, InFlight)
name = namegen.next_pretense_aircraft_name(self.flight.departure, self.flight)
speed = self.flight.state.estimate_speed()
pos = self.flight.state.estimate_position()
pos += Vector2(random.randint(100, 1000), random.randint(100, 1000))
alt, alt_type = self.flight.state.estimate_altitude()
cp = self.flight.squadron.location.id
if cp not in self.mission_data.cp_stack:
self.mission_data.cp_stack[cp] = MINIMUM_MID_MISSION_SPAWN_ALTITUDE_AGL
# We don't know where the ground is, so just make sure that any aircraft
# spawning at an MSL altitude is spawned at some minimum altitude.
# https://github.com/dcs-liberation/dcs_liberation/issues/1941
if alt_type == "BARO" and alt < MINIMUM_MID_MISSION_SPAWN_ALTITUDE_MSL:
alt = MINIMUM_MID_MISSION_SPAWN_ALTITUDE_MSL
# Set a minimum AGL value for 'alt' if needed,
# otherwise planes might crash in trees and stuff.
if alt_type == "RADIO" and alt < self.mission_data.cp_stack[cp]:
alt = self.mission_data.cp_stack[cp]
self.mission_data.cp_stack[cp] += STACK_SEPARATION
self.insert_into_pretense(name)
group = self.mission.flight_group(
country=self.country,
name=name,
aircraft_type=self.flight.unit_type.dcs_unit_type,
airport=None,
position=pos,
altitude=alt.meters,
speed=speed.kph,
maintask=None,
group_size=self.flight.count,
)
group.points[0].alt_type = alt_type
return group

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,296 @@
from __future__ import annotations
import logging
import pickle
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING
import dcs.lua
from dcs import Mission, Point
from dcs.coalition import Coalition
from dcs.countries import (
country_dict,
CombinedJointTaskForcesBlue,
CombinedJointTaskForcesRed,
)
from dcs.task import AFAC, FAC, SetInvisibleCommand, SetImmortalCommand, OrbitAction
from game.lasercodes.lasercoderegistry import LaserCodeRegistry
from game.missiongenerator.convoygenerator import ConvoyGenerator
from game.missiongenerator.environmentgenerator import EnvironmentGenerator
from game.missiongenerator.forcedoptionsgenerator import ForcedOptionsGenerator
from game.missiongenerator.frontlineconflictdescription import (
FrontLineConflictDescription,
)
from game.missiongenerator.missiondata import MissionData, JtacInfo
from game.missiongenerator.tgogenerator import TgoGenerator
from game.missiongenerator.visualsgenerator import VisualsGenerator
from game.naming import namegen
from game.persistency import pre_pretense_backups_dir
from game.pretense.pretenseaircraftgenerator import PretenseAircraftGenerator
from game.radio.radios import RadioRegistry
from game.radio.tacan import TacanRegistry
from game.theater.bullseye import Bullseye
from game.unitmap import UnitMap
from .pretenseluagenerator import PretenseLuaGenerator
from .pretensetgogenerator import PretenseTgoGenerator
from .pretensetriggergenerator import PretenseTriggerGenerator
from ..ato.airtaaskingorder import AirTaskingOrder
from ..callsigns import callsign_for_support_unit
from ..dcs.aircrafttype import AircraftType
from ..missiongenerator import MissionGenerator
from ..theater import Airfield
if TYPE_CHECKING:
from game import Game
class PretenseMissionGenerator(MissionGenerator):
def __init__(self, game: Game, time: datetime) -> None:
super().__init__(game, time)
self.game = game
self.time = time
self.mission = Mission(game.theater.terrain)
self.unit_map = UnitMap()
self.mission_data = MissionData()
self.laser_code_registry = LaserCodeRegistry()
self.radio_registry = RadioRegistry()
self.tacan_registry = TacanRegistry()
self.generation_started = False
self.p_country = country_dict[self.game.blue.faction.country.id]()
self.e_country = country_dict[self.game.red.faction.country.id]()
with open("resources/default_options.lua", "r", encoding="utf-8") as f:
options = dcs.lua.loads(f.read())["options"]
ext_view = game.settings.external_views_allowed
options["miscellaneous"]["f11_free_camera"] = ext_view
options["difficulty"]["spectatorExternalViews"] = ext_view
self.mission.options.load_from_dict(options)
def generate_miz(self, output: Path) -> UnitMap:
game_backup_pickle = pickle.dumps(self.game)
path = pre_pretense_backups_dir()
path.mkdir(parents=True, exist_ok=True)
path /= f".pre-pretense-backup.retribution"
try:
with open(path, "wb") as f:
pickle.dump(self.game, f)
except:
logging.error(f"Unable to save Pretense pre-generation backup to {path}")
if self.generation_started:
raise RuntimeError(
"Mission has already begun generating. To reset, create a new "
"MissionSimulation."
)
self.generation_started = True
self.game.pretense_ground_supply = {1: {}, 2: {}}
self.game.pretense_ground_assault = {1: {}, 2: {}}
self.game.pretense_air = {1: {}, 2: {}}
self.setup_mission_coalitions()
self.add_airfields_to_unit_map()
self.initialize_registries()
EnvironmentGenerator(self.mission, self.game.conditions, self.time).generate()
tgo_generator = PretenseTgoGenerator(
self.mission,
self.game,
self.radio_registry,
self.tacan_registry,
self.unit_map,
self.mission_data,
)
tgo_generator.generate()
ConvoyGenerator(self.mission, self.game, self.unit_map).generate()
# Generate ground conflicts first so the JTACs get the first laser code (1688)
# rather than the first player flight with a TGP.
self.generate_ground_conflicts()
self.generate_air_units(tgo_generator)
for cp in self.game.theater.controlpoints:
if (
self.game.settings.ground_start_airbase_statics_farps_remove
and isinstance(cp, Airfield)
):
while len(tgo_generator.ground_spawns[cp]) > 0:
ground_spawn = tgo_generator.ground_spawns[cp].pop()
# Remove invisible FARPs from airfields because they are unnecessary
neutral_country = self.mission.country(
cp.coalition.game.neutral_country.name
)
neutral_country.remove_static_group(ground_spawn[0])
while len(tgo_generator.ground_spawns_roadbase[cp]) > 0:
ground_spawn = tgo_generator.ground_spawns_roadbase[cp].pop()
# Remove invisible FARPs from airfields because they are unnecessary
neutral_country = self.mission.country(
cp.coalition.game.neutral_country.name
)
neutral_country.remove_static_group(ground_spawn[0])
self.mission.triggerrules.triggers.clear()
PretenseTriggerGenerator(self.mission, self.game).generate()
ForcedOptionsGenerator(self.mission, self.game).generate()
VisualsGenerator(self.mission, self.game).generate()
PretenseLuaGenerator(self.game, self.mission, self.mission_data).generate()
self.setup_combined_arms()
self.notify_info_generators()
# TODO: Shouldn't this be first?
namegen.reset_numbers()
self.mission.save(output)
print(
f"Loading pre-pretense save, number of BLUFOR squadrons: {len(self.game.blue.air_wing.squadrons)}"
)
self.game = pickle.loads(game_backup_pickle)
print(
f"Loaded pre-pretense save, number of BLUFOR squadrons: {len(self.game.blue.air_wing.squadrons)}"
)
self.game.on_load()
return self.unit_map
def setup_mission_coalitions(self) -> None:
self.mission.coalition["blue"] = Coalition(
"blue", bullseye=self.game.blue.bullseye.to_pydcs()
)
self.mission.coalition["red"] = Coalition(
"red", bullseye=self.game.red.bullseye.to_pydcs()
)
self.mission.coalition["neutrals"] = Coalition(
"neutrals", bullseye=Bullseye(Point(0, 0, self.mission.terrain)).to_pydcs()
)
self.mission.coalition["blue"].add_country(self.p_country)
self.mission.coalition["red"].add_country(self.e_country)
# Add CJTF factions to the coalitions, if they're not being used in the campaign
if CombinedJointTaskForcesBlue.id not in {self.p_country.id, self.e_country.id}:
self.mission.coalition["blue"].add_country(CombinedJointTaskForcesBlue())
if CombinedJointTaskForcesRed.id not in {self.p_country.id, self.e_country.id}:
self.mission.coalition["red"].add_country(CombinedJointTaskForcesRed())
belligerents = {self.p_country.id, self.e_country.id}
for country_id in country_dict.keys():
if country_id not in belligerents:
c = country_dict[country_id]()
self.mission.coalition["neutrals"].add_country(c)
def generate_ground_conflicts(self) -> None:
"""Generate FLOTs and JTACs for each active front line."""
for front_line in self.game.theater.conflicts():
player_cp = front_line.blue_cp
enemy_cp = front_line.red_cp
# Add JTAC
if self.game.blue.faction.has_jtac:
freq = self.radio_registry.alloc_uhf()
# If the option fc3LaserCode is enabled, force all JTAC
# laser codes to 1113 to allow lasing for Su-25 Frogfoots and A-10A Warthogs.
# Otherwise use 1688 for the first JTAC, 1687 for the second etc.
if self.game.settings.plugins.get("ctld.fc3LaserCode"):
code = self.game.laser_code_registry.fc3_code
else:
code = front_line.laser_code
utype = self.game.blue.faction.jtac_unit
if utype is None:
utype = AircraftType.named("MQ-9 Reaper")
country = self.mission.country(self.game.blue.faction.country.name)
position = FrontLineConflictDescription.frontline_position(
front_line, self.game.theater, self.game.settings
)
jtac = self.mission.flight_group(
country=country,
name=namegen.next_jtac_name(),
aircraft_type=utype.dcs_unit_type,
position=position[0],
airport=None,
altitude=5000,
maintask=AFAC,
)
jtac.points[0].tasks.append(
FAC(
callsign=len(self.mission_data.jtacs) + 1,
frequency=int(freq.mhz),
modulation=freq.modulation,
)
)
jtac.points[0].tasks.append(SetInvisibleCommand(True))
jtac.points[0].tasks.append(SetImmortalCommand(True))
jtac.points[0].tasks.append(
OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle)
)
frontline = f"Frontline {player_cp.name}/{enemy_cp.name}"
# Note: Will need to change if we ever add ground based JTAC.
callsign = callsign_for_support_unit(jtac)
self.mission_data.jtacs.append(
JtacInfo(
group_name=jtac.name,
unit_name=jtac.units[0].name,
callsign=callsign,
region=frontline,
code=str(code),
blue=True,
freq=freq,
)
)
def generate_air_units(self, tgo_generator: TgoGenerator) -> None:
"""Generate the air units for the Operation"""
# Generate Aircraft Activity on the map
aircraft_generator = PretenseAircraftGenerator(
self.mission,
self.game.settings,
self.game,
self.time,
self.radio_registry,
self.tacan_registry,
self.laser_code_registry,
self.unit_map,
mission_data=self.mission_data,
helipads=tgo_generator.helipads,
ground_spawns_roadbase=tgo_generator.ground_spawns_roadbase,
ground_spawns_large=tgo_generator.ground_spawns_large,
ground_spawns=tgo_generator.ground_spawns,
)
# Clear parking slots and ATOs
aircraft_generator.clear_parking_slots()
self.game.blue.ato.clear()
self.game.red.ato.clear()
for cp in self.game.theater.controlpoints:
for country in (self.p_country, self.e_country):
ato = AirTaskingOrder()
aircraft_generator.generate_flights(
country,
cp,
ato,
)
aircraft_generator.generate_packages(
country,
ato,
tgo_generator.runways,
)
self.mission_data.flights = aircraft_generator.flights
for flight in aircraft_generator.flights:
if not flight.client_units:
continue
flight.aircraft_type.assign_channels_for_flight(flight, self.mission_data)

View File

@ -0,0 +1,955 @@
"""Generators for creating the groups for ground objectives.
The classes in this file are responsible for creating the vehicle groups, ship
groups, statics, missile sites, and AA sites for the mission. Each of these
objectives is defined in the Theater by a TheaterGroundObject. These classes
create the pydcs groups and statics for those areas and add them to the mission.
"""
from __future__ import annotations
import random
import logging
from collections import defaultdict
from typing import Dict, Optional, TYPE_CHECKING, Tuple, Type, Iterator
from dcs import Mission, Point
from dcs.countries import *
from dcs.country import Country
from dcs.ships import Stennis, CVN_71, CVN_72, CVN_73, CVN_75, Forrestal, LHA_Tarawa
from dcs.unitgroup import StaticGroup, VehicleGroup
from dcs.unittype import VehicleType
from game.coalition import Coalition
from game.data.units import UnitClass
from game.dcs.groundunittype import GroundUnitType
from game.missiongenerator.groundforcepainter import (
GroundForcePainter,
)
from game.missiongenerator.missiondata import MissionData, CarrierInfo
from game.missiongenerator.tgogenerator import (
TgoGenerator,
HelipadGenerator,
GroundSpawnRoadbaseGenerator,
GroundSpawnGenerator,
GroundObjectGenerator,
CarrierGenerator,
LhaGenerator,
MissileSiteGenerator,
GenericCarrierGenerator,
)
from game.point_with_heading import PointWithHeading
from game.pretense.pretenseflightgroupspawner import PretenseNameGenerator
from game.radio.radios import RadioRegistry
from game.radio.tacan import TacanRegistry, TacanBand, TacanUsage
from game.runways import RunwayData
from game.theater import (
ControlPoint,
TheaterGroundObject,
TheaterUnit,
NavalControlPoint,
PresetLocation,
)
from game.theater.theatergroundobject import (
CarrierGroundObject,
LhaGroundObject,
MissileSiteGroundObject,
BuildingGroundObject,
VehicleGroupGroundObject,
GenericCarrierGroundObject,
)
from game.theater.theatergroup import TheaterGroup
from game.unitmap import UnitMap
from game.utils import Heading
from pydcs_extensions import (
Char_M551_Sheridan,
BV410_RBS70,
BV410_RBS90,
BV410,
VAB__50,
VAB_T20_13,
)
if TYPE_CHECKING:
from game import Game
FARP_FRONTLINE_DISTANCE = 10000
AA_CP_MIN_DISTANCE = 40000
PRETENSE_GROUND_UNIT_GROUP_SIZE = 5
PRETENSE_GROUND_UNITS_TO_REMOVE_FROM_ASSAULT = [
vehicles.Armor.Stug_III,
vehicles.Artillery.Grad_URAL,
]
PRETENSE_AMPHIBIOUS_UNITS = [
vehicles.Unarmed.LARC_V,
vehicles.Armor.AAV7,
vehicles.Armor.LAV_25,
vehicles.Armor.TPZ,
vehicles.Armor.PT_76,
vehicles.Armor.BMD_1,
vehicles.Armor.BMP_1,
vehicles.Armor.BMP_2,
vehicles.Armor.BMP_3,
vehicles.Armor.BTR_80,
vehicles.Armor.BTR_82A,
vehicles.Armor.BRDM_2,
vehicles.Armor.BTR_D,
vehicles.Armor.MTLB,
vehicles.Armor.ZBD04A,
vehicles.Armor.VAB_Mephisto,
VAB__50,
VAB_T20_13,
Char_M551_Sheridan,
BV410_RBS70,
BV410_RBS90,
BV410,
]
class PretenseGroundObjectGenerator(GroundObjectGenerator):
"""generates the DCS groups and units from the TheaterGroundObject"""
def __init__(
self,
ground_object: TheaterGroundObject,
country: Country,
game: Game,
mission: Mission,
unit_map: UnitMap,
) -> None:
super().__init__(
ground_object,
country,
game,
mission,
unit_map,
)
self.ground_object = ground_object
self.country = country
self.game = game
self.m = mission
self.unit_map = unit_map
self.coalition = ground_object.coalition
@property
def culled(self) -> bool:
return self.game.iads_considerate_culling(self.ground_object)
@staticmethod
def ground_unit_of_class(
coalition: Coalition, unit_class: UnitClass
) -> Optional[GroundUnitType]:
"""
Returns a GroundUnitType of the specified class that belongs to the
TheaterGroundObject faction.
Units, which are known to have pathfinding issues in Pretense missions
are removed based on a pre-defined list.
Args:
coalition: Coalition to return the unit for.
unit_class: Class of unit to return.
"""
faction_units = (
set(coalition.faction.frontline_units)
| set(coalition.faction.artillery_units)
| set(coalition.faction.air_defense_units)
| set(coalition.faction.logistics_units)
)
of_class = list({u for u in faction_units if u.unit_class is unit_class})
# Remove units from list with known pathfinding issues in Pretense missions
for unit_to_remove in PRETENSE_GROUND_UNITS_TO_REMOVE_FROM_ASSAULT:
for groundunittype_to_remove in GroundUnitType.for_dcs_type(unit_to_remove):
if groundunittype_to_remove in of_class:
of_class.remove(groundunittype_to_remove)
if len(of_class) > 0:
return random.choice(of_class)
else:
return None
def generate_ground_unit_of_class(
self,
unit_class: UnitClass,
group: TheaterGroup,
vehicle_units: list[TheaterUnit],
cp_name: str,
group_role: str,
max_num: int,
) -> None:
"""
Generates a single land based TheaterUnit for a Pretense unit group
for a specific TheaterGroup, provided that the group still has room
(defined by the max_num argument). Land based groups don't have
restrictions on the unit types, other than that they must be
accessible by the faction and must be of the specified class.
Generated units are placed 30 meters from the TheaterGroup
position in a random direction.
Args:
unit_class: Class of unit to generate.
group: The TheaterGroup to generate the unit/group for.
vehicle_units: List of TheaterUnits. The new unit will be appended to this list.
cp_name: Name of the Control Point.
group_role: Pretense group role, "support" or "assault".
max_num: Maximum number of units to generate per group.
"""
if self.coalition.faction.has_access_to_unit_class(unit_class):
unit_type = self.ground_unit_of_class(self.coalition, unit_class)
if unit_type is not None and len(vehicle_units) < max_num:
unit_id = self.game.next_unit_id()
unit_name = f"{cp_name}-{group_role}-{unit_id}"
spread_out_heading = random.randrange(1, 360)
spread_out_position = group.position.point_from_heading(
spread_out_heading, 30
)
ground_unit_pos = PointWithHeading.from_point(
spread_out_position, group.position.heading
)
theater_unit = TheaterUnit(
unit_id,
unit_name,
unit_type.dcs_unit_type,
ground_unit_pos,
group.ground_object,
)
vehicle_units.append(theater_unit)
def generate_amphibious_unit_of_class(
self,
unit_class: UnitClass,
group: TheaterGroup,
vehicle_units: list[TheaterUnit],
cp_name: str,
group_role: str,
max_num: int,
) -> None:
"""
Generates a single amphibious TheaterUnit for a Pretense unit group
for a specific TheaterGroup, provided that the group still has room
(defined by the max_num argument). Amphibious units are selected
out of a pre-defined list. Units which the faction has access to
are preferred, but certain default unit types are selected as
a fall-back to ensure that all the generated units can swim.
Generated units are placed 30 meters from the TheaterGroup
position in a random direction.
Args:
unit_class: Class of unit to generate.
group: The TheaterGroup to generate the unit/group for.
vehicle_units: List of TheaterUnits. The new unit will be appended to this list.
cp_name: Name of the Control Point.
group_role: Pretense group role, "support" or "assault".
max_num: Maximum number of units to generate per group.
"""
unit_type = None
faction = self.coalition.faction
is_player = True
side = (
2
if self.country == self.game.coalition_for(is_player).faction.country
else 1
)
default_amphibious_unit = unit_type
default_logistics_unit = unit_type
default_tank_unit_blue = unit_type
default_apc_unit_blue = unit_type
default_ifv_unit_blue = unit_type
default_recon_unit_blue = unit_type
default_atgm_unit_blue = unit_type
default_tank_unit_red = unit_type
default_apc_unit_red = unit_type
default_ifv_unit_red = unit_type
default_recon_unit_red = unit_type
default_atgm_unit_red = unit_type
default_ifv_unit_chinese = unit_type
pretense_amphibious_units = PRETENSE_AMPHIBIOUS_UNITS
random.shuffle(pretense_amphibious_units)
for unit in pretense_amphibious_units:
for groundunittype in GroundUnitType.for_dcs_type(unit):
if unit == vehicles.Unarmed.LARC_V:
default_logistics_unit = groundunittype
elif unit == Char_M551_Sheridan:
default_tank_unit_blue = groundunittype
elif unit == vehicles.Armor.AAV7:
default_apc_unit_blue = groundunittype
elif unit == vehicles.Armor.LAV_25:
default_ifv_unit_blue = groundunittype
elif unit == vehicles.Armor.TPZ:
default_recon_unit_blue = groundunittype
elif unit == vehicles.Armor.VAB_Mephisto:
default_atgm_unit_blue = groundunittype
elif unit == vehicles.Armor.PT_76:
default_tank_unit_red = groundunittype
elif unit == vehicles.Armor.BTR_80:
default_apc_unit_red = groundunittype
elif unit == vehicles.Armor.BMD_1:
default_ifv_unit_red = groundunittype
elif unit == vehicles.Armor.BRDM_2:
default_recon_unit_red = groundunittype
elif unit == vehicles.Armor.BTR_D:
default_atgm_unit_red = groundunittype
elif unit == vehicles.Armor.ZBD04A:
default_ifv_unit_chinese = groundunittype
elif unit == vehicles.Armor.MTLB:
default_amphibious_unit = groundunittype
if self.coalition.faction.has_access_to_dcs_type(unit):
if groundunittype.unit_class == unit_class:
unit_type = groundunittype
break
if unit_type is None:
if unit_class == UnitClass.LOGISTICS:
unit_type = default_logistics_unit
elif faction.country.id == China.id:
unit_type = default_ifv_unit_chinese
elif side == 2 and unit_class == UnitClass.TANK:
if faction.mod_settings is not None and faction.mod_settings.frenchpack:
unit_type = default_tank_unit_blue
else:
unit_type = default_apc_unit_blue
elif side == 2 and unit_class == UnitClass.IFV:
unit_type = default_ifv_unit_blue
elif side == 2 and unit_class == UnitClass.APC:
unit_type = default_apc_unit_blue
elif side == 2 and unit_class == UnitClass.ATGM:
unit_type = default_atgm_unit_blue
elif side == 2 and unit_class == UnitClass.RECON:
unit_type = default_recon_unit_blue
elif side == 1 and unit_class == UnitClass.TANK:
unit_type = default_tank_unit_red
elif side == 1 and unit_class == UnitClass.IFV:
unit_type = default_ifv_unit_red
elif side == 1 and unit_class == UnitClass.APC:
unit_type = default_apc_unit_red
elif side == 1 and unit_class == UnitClass.ATGM:
unit_type = default_atgm_unit_red
elif side == 1 and unit_class == UnitClass.RECON:
unit_type = default_recon_unit_red
else:
unit_type = default_amphibious_unit
if unit_type is not None and len(vehicle_units) < max_num:
unit_id = self.game.next_unit_id()
unit_name = f"{cp_name}-{group_role}-{unit_id}"
spread_out_heading = random.randrange(1, 360)
spread_out_position = group.position.point_from_heading(
spread_out_heading, 30
)
ground_unit_pos = PointWithHeading.from_point(
spread_out_position, group.position.heading
)
theater_unit = TheaterUnit(
unit_id,
unit_name,
unit_type.dcs_unit_type,
ground_unit_pos,
group.ground_object,
)
vehicle_units.append(theater_unit)
def generate(self) -> None:
if self.culled:
return
cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(
self.ground_object.control_point.name
)
country_name_trimmed = "".join(
[i for i in self.country.shortname.lower() if i.isalpha()]
)
for group in self.ground_object.groups:
vehicle_units: list[TheaterUnit] = []
for unit in group.units:
if unit.is_static:
# Add supply convoy
group_role = "supply"
group_name = f"{cp_name_trimmed}-{country_name_trimmed}-{group_role}-{group.id}"
group.name = group_name
self.generate_ground_unit_of_class(
UnitClass.LOGISTICS,
group,
vehicle_units,
cp_name_trimmed,
group_role,
PRETENSE_GROUND_UNIT_GROUP_SIZE,
)
elif unit.is_vehicle and unit.alive:
# Add armor group
group_role = "assault"
group_name = f"{cp_name_trimmed}-{country_name_trimmed}-{group_role}-{group.id}"
group.name = group_name
self.generate_ground_unit_of_class(
UnitClass.TANK,
group,
vehicle_units,
cp_name_trimmed,
group_role,
PRETENSE_GROUND_UNIT_GROUP_SIZE - 4,
)
self.generate_ground_unit_of_class(
UnitClass.TANK,
group,
vehicle_units,
cp_name_trimmed,
group_role,
PRETENSE_GROUND_UNIT_GROUP_SIZE - 3,
)
self.generate_ground_unit_of_class(
UnitClass.ATGM,
group,
vehicle_units,
cp_name_trimmed,
group_role,
PRETENSE_GROUND_UNIT_GROUP_SIZE - 2,
)
self.generate_ground_unit_of_class(
UnitClass.APC,
group,
vehicle_units,
cp_name_trimmed,
group_role,
PRETENSE_GROUND_UNIT_GROUP_SIZE - 1,
)
self.generate_ground_unit_of_class(
UnitClass.IFV,
group,
vehicle_units,
cp_name_trimmed,
group_role,
PRETENSE_GROUND_UNIT_GROUP_SIZE,
)
self.generate_ground_unit_of_class(
UnitClass.RECON,
group,
vehicle_units,
cp_name_trimmed,
group_role,
PRETENSE_GROUND_UNIT_GROUP_SIZE,
)
if random.randrange(0, 100) > 75:
self.generate_ground_unit_of_class(
UnitClass.SHORAD,
group,
vehicle_units,
cp_name_trimmed,
group_role,
PRETENSE_GROUND_UNIT_GROUP_SIZE,
)
elif unit.is_ship and unit.alive:
# Attach this group to the closest naval group, if available
control_point = self.ground_object.control_point
for (
other_cp
) in self.game.theater.closest_friendly_control_points_to(
self.ground_object.control_point
):
if other_cp.is_fleet:
control_point = other_cp
break
cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(
control_point.name
)
is_player = True
side = (
2
if self.country
== self.game.coalition_for(is_player).faction.country
else 1
)
try:
number_of_supply_groups = len(
self.game.pretense_ground_supply[side][cp_name_trimmed]
)
except KeyError:
number_of_supply_groups = 0
self.game.pretense_ground_supply[side][cp_name_trimmed] = list()
self.game.pretense_ground_assault[side][
cp_name_trimmed
] = list()
if number_of_supply_groups == 0:
# Add supply convoy
group_role = "supply"
group_name = f"{cp_name_trimmed}-{country_name_trimmed}-{group_role}-{group.id}"
group.name = group_name
self.generate_amphibious_unit_of_class(
UnitClass.LOGISTICS,
group,
vehicle_units,
cp_name_trimmed,
group_role,
PRETENSE_GROUND_UNIT_GROUP_SIZE,
)
else:
# Add armor group
group_role = "assault"
group_name = f"{cp_name_trimmed}-{country_name_trimmed}-{group_role}-{group.id}"
group.name = group_name
self.generate_amphibious_unit_of_class(
UnitClass.TANK,
group,
vehicle_units,
cp_name_trimmed,
group_role,
PRETENSE_GROUND_UNIT_GROUP_SIZE - 4,
)
self.generate_amphibious_unit_of_class(
UnitClass.TANK,
group,
vehicle_units,
cp_name_trimmed,
group_role,
PRETENSE_GROUND_UNIT_GROUP_SIZE - 3,
)
self.generate_amphibious_unit_of_class(
UnitClass.ATGM,
group,
vehicle_units,
cp_name_trimmed,
group_role,
PRETENSE_GROUND_UNIT_GROUP_SIZE - 2,
)
self.generate_amphibious_unit_of_class(
UnitClass.APC,
group,
vehicle_units,
cp_name_trimmed,
group_role,
PRETENSE_GROUND_UNIT_GROUP_SIZE - 1,
)
self.generate_amphibious_unit_of_class(
UnitClass.IFV,
group,
vehicle_units,
cp_name_trimmed,
group_role,
PRETENSE_GROUND_UNIT_GROUP_SIZE,
)
self.generate_amphibious_unit_of_class(
UnitClass.RECON,
group,
vehicle_units,
cp_name_trimmed,
group_role,
PRETENSE_GROUND_UNIT_GROUP_SIZE,
)
if vehicle_units:
self.create_vehicle_group(group.group_name, vehicle_units)
def create_vehicle_group(
self, group_name: str, units: list[TheaterUnit]
) -> VehicleGroup:
vehicle_group: Optional[VehicleGroup] = None
control_point = self.ground_object.control_point
for unit in self.ground_object.units:
if unit.is_ship:
# Unit is naval/amphibious. Attach this group to the closest naval group, if available.
for other_cp in self.game.theater.closest_friendly_control_points_to(
self.ground_object.control_point
):
if other_cp.is_fleet:
control_point = other_cp
break
cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(
control_point.name
)
is_player = True
side = (
2
if self.country == self.game.coalition_for(is_player).faction.country
else 1
)
for unit in units:
assert issubclass(unit.type, VehicleType)
faction = self.coalition.faction
if vehicle_group is None:
vehicle_group = self.m.vehicle_group(
self.country,
group_name,
unit.type,
position=unit.position,
heading=unit.position.heading.degrees,
)
vehicle_group.units[0].player_can_drive = True
self.enable_eplrs(vehicle_group, unit.type)
vehicle_group.units[0].name = unit.unit_name
self.set_alarm_state(vehicle_group)
GroundForcePainter(faction, vehicle_group.units[0]).apply_livery()
group_role = group_name.split("-")[2]
if group_role == "supply":
self.game.pretense_ground_supply[side][cp_name_trimmed].append(
f"{vehicle_group.name}"
)
elif group_role == "assault":
self.game.pretense_ground_assault[side][cp_name_trimmed].append(
f"{vehicle_group.name}"
)
else:
vehicle_unit = self.m.vehicle(unit.unit_name, unit.type)
vehicle_unit.player_can_drive = True
vehicle_unit.position = unit.position
vehicle_unit.heading = unit.position.heading.degrees
GroundForcePainter(faction, vehicle_unit).apply_livery()
vehicle_group.add_unit(vehicle_unit)
self._register_theater_unit(unit, vehicle_group.units[-1])
if vehicle_group is None:
raise RuntimeError(f"Error creating VehicleGroup for {group_name}")
return vehicle_group
class PretenseGenericCarrierGenerator(GenericCarrierGenerator):
"""Base type for carrier group generation.
Used by both CV(N) groups and LHA groups.
"""
def __init__(
self,
ground_object: GenericCarrierGroundObject,
control_point: NavalControlPoint,
country: Country,
game: Game,
mission: Mission,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
icls_alloc: Iterator[int],
runways: Dict[str, RunwayData],
unit_map: UnitMap,
mission_data: MissionData,
) -> None:
super().__init__(
ground_object,
control_point,
country,
game,
mission,
radio_registry,
tacan_registry,
icls_alloc,
runways,
unit_map,
mission_data,
)
self.ground_object = ground_object
self.control_point = control_point
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
self.icls_alloc = icls_alloc
self.runways = runways
self.mission_data = mission_data
def generate(self) -> None:
if self.control_point.frequency is not None:
atc = self.control_point.frequency
if atc not in self.radio_registry.allocated_channels:
self.radio_registry.reserve(atc)
else:
atc = self.radio_registry.alloc_uhf()
for g_id, group in enumerate(self.ground_object.groups):
if not group.units:
logging.warning(f"Found empty carrier group in {self.control_point}")
continue
ship_units = []
for unit in group.units:
if unit.alive:
# All alive Ships
print(
f"Added {unit.unit_name} to ship_units of group {group.group_name}"
)
ship_units.append(unit)
if not ship_units:
# Empty array (no alive units), skip this group
continue
ship_group = self.create_ship_group(group.group_name, ship_units, atc)
if self.game.settings.pretense_carrier_steams_into_wind:
# Always steam into the wind, even if the carrier is being moved.
# There are multiple unsimulated hours between turns, so we can
# count those as the time the carrier uses to move and the mission
# time as the recovery window.
brc = self.steam_into_wind(ship_group)
else:
brc = Heading(0)
# Set Carrier Specific Options
if g_id == 0 and self.control_point.runway_is_operational():
# Get Correct unit type for the carrier.
# This will upgrade to super carrier if option is enabled
carrier_type = self.carrier_type
if carrier_type is None:
raise RuntimeError(
f"Error generating carrier group for {self.control_point.name}"
)
ship_group.units[0].type = carrier_type.id
if self.control_point.tacan is None:
tacan = self.tacan_registry.alloc_for_band(
TacanBand.X, TacanUsage.TransmitReceive
)
else:
tacan = self.control_point.tacan
if self.control_point.tcn_name is None:
tacan_callsign = self.tacan_callsign()
else:
tacan_callsign = self.control_point.tcn_name
link4 = None
link4carriers = [Stennis, CVN_71, CVN_72, CVN_73, CVN_75, Forrestal]
if carrier_type in link4carriers:
if self.control_point.link4 is None:
link4 = self.radio_registry.alloc_uhf()
else:
link4 = self.control_point.link4
icls = None
icls_name = self.control_point.icls_name
if carrier_type in link4carriers or carrier_type == LHA_Tarawa:
if self.control_point.icls_channel is None:
icls = next(self.icls_alloc)
else:
icls = self.control_point.icls_channel
self.activate_beacons(
ship_group, tacan, tacan_callsign, icls, icls_name, link4
)
self.add_runway_data(
brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls
)
self.mission_data.carriers.append(
CarrierInfo(
group_name=ship_group.name,
unit_name=ship_group.units[0].name,
callsign=tacan_callsign,
freq=atc,
tacan=tacan,
icls_channel=icls,
link4_freq=link4,
blue=self.control_point.captured,
)
)
class PretenseCarrierGenerator(PretenseGenericCarrierGenerator):
def tacan_callsign(self) -> str:
# TODO: Assign these properly.
return random.choice(
[
"STE",
"CVN",
"CVH",
"CCV",
"ACC",
"ARC",
"GER",
"ABR",
"LIN",
"TRU",
]
)
class PretenseLhaGenerator(PretenseGenericCarrierGenerator):
def tacan_callsign(self) -> str:
# TODO: Assign these properly.
return random.choice(
[
"LHD",
"LHA",
"LHB",
"LHC",
"LHD",
"LDS",
]
)
class PretenseTgoGenerator(TgoGenerator):
"""Creates DCS groups and statics for the theater during mission generation.
Most of the work of group/static generation is delegated to the other
generator classes. This class is responsible for finding each of the
locations for spawning ground objects, determining their types, and creating
the appropriate generators.
"""
def __init__(
self,
mission: Mission,
game: Game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
unit_map: UnitMap,
mission_data: MissionData,
) -> None:
super().__init__(
mission,
game,
radio_registry,
tacan_registry,
unit_map,
mission_data,
)
self.m = mission
self.game = game
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
self.unit_map = unit_map
self.icls_alloc = iter(range(1, 21))
self.runways: Dict[str, RunwayData] = {}
self.helipads: dict[ControlPoint, list[StaticGroup]] = defaultdict(list)
self.ground_spawns_roadbase: dict[
ControlPoint, list[Tuple[StaticGroup, Point]]
] = defaultdict(list)
self.ground_spawns: dict[
ControlPoint, list[Tuple[StaticGroup, Point]]
] = defaultdict(list)
self.mission_data = mission_data
def generate(self) -> None:
for cp in self.game.theater.controlpoints:
cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(cp.name)
for side in range(1, 3):
if cp_name_trimmed not in self.game.pretense_ground_supply[side]:
self.game.pretense_ground_supply[side][cp_name_trimmed] = list()
if cp_name_trimmed not in self.game.pretense_ground_assault[side]:
self.game.pretense_ground_assault[side][cp_name_trimmed] = list()
# First generate units for the coalition, which initially holds this CP
country = self.m.country(cp.coalition.faction.country.name)
# Generate helipads
helipad_gen = HelipadGenerator(
self.m, cp, self.game, self.radio_registry, self.tacan_registry
)
helipad_gen.generate()
self.helipads[cp] = helipad_gen.helipads
# Generate Highway Strip slots
ground_spawn_roadbase_gen = GroundSpawnRoadbaseGenerator(
self.m, cp, self.game, self.radio_registry, self.tacan_registry
)
ground_spawn_roadbase_gen.generate()
self.ground_spawns_roadbase[
cp
] = ground_spawn_roadbase_gen.ground_spawns_roadbase
random.shuffle(self.ground_spawns_roadbase[cp])
# Generate STOL pads
ground_spawn_gen = GroundSpawnGenerator(
self.m, cp, self.game, self.radio_registry, self.tacan_registry
)
ground_spawn_gen.generate()
self.ground_spawns[cp] = ground_spawn_gen.ground_spawns
random.shuffle(self.ground_spawns[cp])
for ground_object in cp.ground_objects:
generator: GroundObjectGenerator
if isinstance(ground_object, CarrierGroundObject) and isinstance(
cp, NavalControlPoint
):
generator = PretenseCarrierGenerator(
ground_object,
cp,
country,
self.game,
self.m,
self.radio_registry,
self.tacan_registry,
self.icls_alloc,
self.runways,
self.unit_map,
self.mission_data,
)
elif isinstance(ground_object, LhaGroundObject) and isinstance(
cp, NavalControlPoint
):
generator = PretenseLhaGenerator(
ground_object,
cp,
country,
self.game,
self.m,
self.radio_registry,
self.tacan_registry,
self.icls_alloc,
self.runways,
self.unit_map,
self.mission_data,
)
elif isinstance(ground_object, MissileSiteGroundObject):
generator = MissileSiteGenerator(
ground_object, country, self.game, self.m, self.unit_map
)
else:
generator = PretenseGroundObjectGenerator(
ground_object, country, self.game, self.m, self.unit_map
)
generator.generate()
# Then generate ground supply and assault groups for the other coalition
other_coalition = cp.coalition
for coalition in cp.coalition.game.coalitions:
if coalition == cp.coalition:
continue
else:
other_coalition = coalition
country = self.m.country(other_coalition.faction.country.name)
new_ground_object: TheaterGroundObject
for ground_object in cp.ground_objects:
if isinstance(ground_object, BuildingGroundObject):
new_ground_object = BuildingGroundObject(
name=ground_object.name,
category=ground_object.category,
location=PresetLocation(
f"{ground_object.name} {ground_object.id}",
ground_object.position,
ground_object.heading,
),
control_point=ground_object.control_point,
is_fob_structure=ground_object.is_fob_structure,
task=ground_object.task,
)
new_ground_object.groups = ground_object.groups
generator = PretenseGroundObjectGenerator(
new_ground_object, country, self.game, self.m, self.unit_map
)
elif isinstance(ground_object, VehicleGroupGroundObject):
new_ground_object = VehicleGroupGroundObject(
name=ground_object.name,
location=PresetLocation(
f"{ground_object.name} {ground_object.id}",
ground_object.position,
ground_object.heading,
),
control_point=ground_object.control_point,
task=ground_object.task,
)
new_ground_object.groups = ground_object.groups
generator = PretenseGroundObjectGenerator(
new_ground_object, country, self.game, self.m, self.unit_map
)
else:
continue
generator.coalition = other_coalition
generator.generate()
self.mission_data.runways = list(self.runways.values())

View File

@ -0,0 +1,475 @@
from __future__ import annotations
import logging
import math
import random
from typing import TYPE_CHECKING, List
from dcs import Point
from dcs.action import (
ClearFlag,
DoScript,
MarkToAll,
SetFlag,
RemoveSceneObjects,
RemoveSceneObjectsMask,
SceneryDestructionZone,
Smoke,
)
from dcs.condition import (
AllOfCoalitionOutsideZone,
FlagIsFalse,
FlagIsTrue,
PartOfCoalitionInZone,
TimeAfter,
TimeSinceFlag,
)
from dcs.mission import Mission
from dcs.task import Option
from dcs.terrain.caucasus.airports import Krasnodar_Pashkovsky
from dcs.terrain.syria.airports import Damascus, Khalkhalah
from dcs.translation import String
from dcs.triggers import Event, TriggerCondition, TriggerOnce
from dcs.unit import Skill
from numpy import cross, einsum, arctan2
from shapely import MultiPolygon, Point as ShapelyPoint
from game.naming import ALPHA_MILITARY
from game.pretense.pretenseflightgroupspawner import PretenseNameGenerator
from game.theater import Airfield
from game.theater.controlpoint import Fob, TRIGGER_RADIUS_CAPTURE, OffMapSpawn
if TYPE_CHECKING:
from game.game import Game
PUSH_TRIGGER_SIZE = 3000
PUSH_TRIGGER_ACTIVATION_AGL = 25
REGROUP_ZONE_DISTANCE = 12000
REGROUP_ALT = 5000
TRIGGER_WAYPOINT_OFFSET = 2
TRIGGER_MIN_DISTANCE_FROM_START = 10000
# modified since we now have advanced SAM units
TRIGGER_RADIUS_MINIMUM = 3000000
TRIGGER_RADIUS_SMALL = 50000
TRIGGER_RADIUS_MEDIUM = 100000
TRIGGER_RADIUS_LARGE = 150000
TRIGGER_RADIUS_ALL_MAP = 3000000
TRIGGER_RADIUS_CLEAR_SCENERY = 1000
TRIGGER_RADIUS_PRETENSE_TGO = 500
TRIGGER_RADIUS_PRETENSE_SUPPLY = 500
TRIGGER_RADIUS_PRETENSE_HELI = 1000
TRIGGER_RADIUS_PRETENSE_HELI_BUFFER = 500
TRIGGER_RADIUS_PRETENSE_CARRIER = 20000
TRIGGER_RADIUS_PRETENSE_CARRIER_SMALL = 3000
TRIGGER_RADIUS_PRETENSE_CARRIER_CORNER = 25000
TRIGGER_RUNWAY_LENGTH_PRETENSE = 2500
TRIGGER_RUNWAY_WIDTH_PRETENSE = 400
SIMPLIFY_RUNS_PRETENSE_CARRIER = 10000
class Silence(Option):
Key = 7
class PretenseTriggerGenerator:
capture_zone_types = (Fob, Airfield)
capture_zone_flag = 600
def __init__(self, mission: Mission, game: Game) -> None:
self.mission = mission
self.game = game
def _set_allegiances(self, player_coalition: str, enemy_coalition: str) -> None:
"""
Set airbase initial coalition
"""
# Empty neutrals airports
airfields = [
cp for cp in self.game.theater.controlpoints if isinstance(cp, Airfield)
]
airport_ids = {cp.airport.id for cp in airfields}
for airport in self.mission.terrain.airport_list():
if airport.id not in airport_ids:
airport.unlimited_fuel = False
airport.unlimited_munitions = False
airport.unlimited_aircrafts = False
airport.gasoline_init = 0
airport.methanol_mixture_init = 0
airport.diesel_init = 0
airport.jet_init = 0
airport.operating_level_air = 0
airport.operating_level_equipment = 0
airport.operating_level_fuel = 0
for airport in self.mission.terrain.airport_list():
if airport.id not in airport_ids:
airport.unlimited_fuel = True
airport.unlimited_munitions = True
airport.unlimited_aircrafts = True
for airfield in airfields:
cp_airport = self.mission.terrain.airport_by_id(airfield.airport.id)
if cp_airport is None:
raise RuntimeError(
f"Could not find {airfield.airport.name} in the mission"
)
cp_airport.set_coalition(
airfield.captured and player_coalition or enemy_coalition
)
def _set_skill(self, player_coalition: str, enemy_coalition: str) -> None:
"""
Set skill level for all aircraft in the mission
"""
for coalition_name, coalition in self.mission.coalition.items():
if coalition_name == player_coalition:
skill_level = Skill(self.game.settings.player_skill)
elif coalition_name == enemy_coalition:
skill_level = Skill(self.game.settings.enemy_vehicle_skill)
else:
continue
for country in coalition.countries.values():
for vehicle_group in country.vehicle_group:
vehicle_group.set_skill(skill_level)
def _gen_markers(self) -> None:
"""
Generate markers on F10 map for each existing objective
"""
if self.game.settings.generate_marks:
mark_trigger = TriggerOnce(Event.NoEvent, "Marks generator")
mark_trigger.add_condition(TimeAfter(1))
v = 10
for cp in self.game.theater.controlpoints:
seen = set()
for ground_object in cp.ground_objects:
if ground_object.obj_name in seen:
continue
seen.add(ground_object.obj_name)
for location in ground_object.mark_locations:
zone = self.mission.triggers.add_triggerzone(
location, radius=10, hidden=True, name="MARK"
)
if cp.captured:
name = ground_object.obj_name + " [ALLY]"
else:
name = ground_object.obj_name + " [ENEMY]"
mark_trigger.add_action(MarkToAll(v, zone.id, String(name)))
v += 1
self.mission.triggerrules.triggers.append(mark_trigger)
def _generate_capture_triggers(
self, player_coalition: str, enemy_coalition: str
) -> None:
"""Creates a pair of triggers for each control point of `cls.capture_zone_types`.
One for the initial capture of a control point, and one if it is recaptured.
Directly appends to the global `base_capture_events` var declared by `dcs_libaration.lua`
"""
for cp in self.game.theater.controlpoints:
if isinstance(cp, self.capture_zone_types) and not cp.is_carrier:
if cp.captured:
attacking_coalition = enemy_coalition
attack_coalition_int = 1 # 1 is the Event int for Red
defending_coalition = player_coalition
defend_coalition_int = 2 # 2 is the Event int for Blue
else:
attacking_coalition = player_coalition
attack_coalition_int = 2
defending_coalition = enemy_coalition
defend_coalition_int = 1
trigger_zone = self.mission.triggers.add_triggerzone(
cp.position,
radius=TRIGGER_RADIUS_CAPTURE,
hidden=False,
name="CAPTURE",
)
flag = self.get_capture_zone_flag()
capture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
capture_trigger.add_condition(
AllOfCoalitionOutsideZone(
defending_coalition, trigger_zone.id, unit_type="GROUND"
)
)
capture_trigger.add_condition(
PartOfCoalitionInZone(
attacking_coalition, trigger_zone.id, unit_type="GROUND"
)
)
capture_trigger.add_condition(FlagIsFalse(flag=flag))
script_string = String(
f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{attack_coalition_int}||{cp.full_name}"'
)
capture_trigger.add_action(DoScript(script_string))
capture_trigger.add_action(SetFlag(flag=flag))
self.mission.triggerrules.triggers.append(capture_trigger)
recapture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
recapture_trigger.add_condition(
AllOfCoalitionOutsideZone(
attacking_coalition, trigger_zone.id, unit_type="GROUND"
)
)
recapture_trigger.add_condition(
PartOfCoalitionInZone(
defending_coalition, trigger_zone.id, unit_type="GROUND"
)
)
recapture_trigger.add_condition(FlagIsTrue(flag=flag))
script_string = String(
f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{defend_coalition_int}||{cp.full_name}"'
)
recapture_trigger.add_action(DoScript(script_string))
recapture_trigger.add_action(ClearFlag(flag=flag))
self.mission.triggerrules.triggers.append(recapture_trigger)
def _generate_pretense_zone_triggers(self) -> None:
"""Creates triggger zones for the Pretense campaign. These include:
- Carrier zones for friendly forces, generated from the navmesh / sea zone intersection
- Carrier zones for opposing forces
- Airfield and FARP zones
- Airfield and FARP spawn points / helicopter spawn points / ground object positions
"""
# First generate carrier zones for friendly forces
use_blue_navmesh = (
self.game.settings.pretense_carrier_zones_navmesh == "Blue navmesh"
)
sea_zones_landmap = self.game.coalition_for(
player=False
).nav_mesh.theater.landmap
if (
self.game.settings.pretense_controllable_carrier
and sea_zones_landmap is not None
):
navmesh_number = 0
for navmesh_poly in self.game.coalition_for(
player=use_blue_navmesh
).nav_mesh.polys:
navmesh_number += 1
if sea_zones_landmap.sea_zones.intersects(navmesh_poly.poly):
# Get the intersection between the navmesh zone and the sea zone
navmesh_sea_intersection = sea_zones_landmap.sea_zones.intersection(
navmesh_poly.poly
)
navmesh_zone_verticies = navmesh_sea_intersection
# Simplify it to get a quadrangle
for simplify_run in range(SIMPLIFY_RUNS_PRETENSE_CARRIER):
navmesh_zone_verticies = navmesh_sea_intersection.simplify(
float(simplify_run * 10), preserve_topology=False
)
if isinstance(navmesh_zone_verticies, MultiPolygon):
break
if len(navmesh_zone_verticies.exterior.coords) <= 4:
break
if isinstance(navmesh_zone_verticies, MultiPolygon):
continue
trigger_zone_verticies = []
terrain = self.game.theater.terrain
alpha = random.choice(ALPHA_MILITARY)
# Generate the quadrangle zone and four points inside it for carrier navigation
if len(navmesh_zone_verticies.exterior.coords) == 4:
zone_color = {1: 1.0, 2: 1.0, 3: 1.0, 4: 0.15}
corner_point_num = 0
for point_coord in navmesh_zone_verticies.exterior.coords:
corner_point = Point(
x=point_coord[0], y=point_coord[1], terrain=terrain
)
nav_point = corner_point.point_from_heading(
corner_point.heading_between_point(
navmesh_sea_intersection.centroid
),
TRIGGER_RADIUS_PRETENSE_CARRIER_CORNER,
)
corner_point_num += 1
zone_name = f"{alpha}-{navmesh_number}-{corner_point_num}"
if sea_zones_landmap.sea_zones.contains(
ShapelyPoint(nav_point.x, nav_point.y)
):
self.mission.triggers.add_triggerzone(
nav_point,
radius=TRIGGER_RADIUS_PRETENSE_CARRIER_SMALL,
hidden=False,
name=zone_name,
color=zone_color,
)
trigger_zone_verticies.append(corner_point)
zone_name = f"{alpha}-{navmesh_number}"
trigger_zone = self.mission.triggers.add_triggerzone_quad(
navmesh_sea_intersection.centroid,
trigger_zone_verticies,
hidden=False,
name=zone_name,
color=zone_color,
)
try:
if len(self.game.pretense_carrier_zones) == 0:
self.game.pretense_carrier_zones = []
except AttributeError:
self.game.pretense_carrier_zones = []
self.game.pretense_carrier_zones.append(zone_name)
for cp in self.game.theater.controlpoints:
if (
cp.is_fleet
and self.game.settings.pretense_controllable_carrier
and cp.captured
):
# Friendly carrier zones are generated above
continue
elif cp.is_fleet:
trigger_radius = float(TRIGGER_RADIUS_PRETENSE_CARRIER)
elif isinstance(cp, Fob) and cp.has_helipads:
trigger_radius = TRIGGER_RADIUS_PRETENSE_HELI
for helipad in list(
cp.helipads + cp.helipads_quad + cp.helipads_invisible
):
if cp.position.distance_to_point(helipad) > trigger_radius:
trigger_radius = cp.position.distance_to_point(helipad)
for ground_spawn, ground_spawn_wp in list(
cp.ground_spawns + cp.ground_spawns_roadbase
):
if cp.position.distance_to_point(ground_spawn) > trigger_radius:
trigger_radius = cp.position.distance_to_point(ground_spawn)
trigger_radius += TRIGGER_RADIUS_PRETENSE_HELI_BUFFER
else:
if cp.dcs_airport is not None and (
isinstance(cp.dcs_airport, Damascus)
or isinstance(cp.dcs_airport, Khalkhalah)
or isinstance(cp.dcs_airport, Krasnodar_Pashkovsky)
):
# Increase the size of Pretense zones at Damascus, Khalkhalah and Krasnodar-Pashkovsky
# (which are quite spread out) so the zone would encompass the entire airfield.
trigger_radius = int(TRIGGER_RADIUS_CAPTURE * 1.8)
else:
trigger_radius = TRIGGER_RADIUS_CAPTURE
cp_name = "".join(
[i for i in cp.name if i.isalnum() or i.isspace() or i == "-"]
)
cp_name = cp_name.replace("Ä", "A")
cp_name = cp_name.replace("Ö", "O")
cp_name = cp_name.replace("Ø", "O")
cp_name = cp_name.replace("ä", "a")
cp_name = cp_name.replace("ö", "o")
cp_name = cp_name.replace("ø", "o")
if not isinstance(cp, OffMapSpawn):
zone_color = {1: 0.0, 2: 0.0, 3: 0.0, 4: 0.15}
self.mission.triggers.add_triggerzone(
cp.position,
radius=trigger_radius,
hidden=False,
name=cp_name,
color=zone_color,
)
cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(cp.name)
tgo_num = 0
for tgo in cp.ground_objects:
if cp.is_fleet or tgo.sea_object:
continue
tgo_num += 1
zone_color = {1: 1.0, 2: 1.0, 3: 1.0, 4: 0.15}
self.mission.triggers.add_triggerzone(
tgo.position,
radius=TRIGGER_RADIUS_PRETENSE_TGO,
hidden=False,
name=f"{cp_name_trimmed}-{tgo_num}",
color=zone_color,
)
for helipad in cp.helipads + cp.helipads_invisible + cp.helipads_quad:
zone_color = {1: 1.0, 2: 1.0, 3: 1.0, 4: 0.15}
self.mission.triggers.add_triggerzone(
position=helipad,
radius=TRIGGER_RADIUS_PRETENSE_HELI,
hidden=False,
name=f"{cp_name_trimmed}-hsp",
color=zone_color,
)
break
for supply_route in cp.convoy_routes.values():
tgo_num += 1
zone_color = {1: 1.0, 2: 1.0, 3: 1.0, 4: 0.15}
origin_position = supply_route[0]
next_position = supply_route[1]
convoy_heading = origin_position.heading_between_point(next_position)
supply_position = origin_position.point_from_heading(
convoy_heading, 300
)
self.mission.triggers.add_triggerzone(
supply_position,
radius=TRIGGER_RADIUS_PRETENSE_TGO,
hidden=False,
name=f"{cp_name_trimmed}-sp",
color=zone_color,
)
break
airfields = [
cp for cp in self.game.theater.controlpoints if isinstance(cp, Airfield)
]
for airfield in airfields:
cp_airport = self.mission.terrain.airport_by_id(airfield.airport.id)
if cp_airport is None:
continue
cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(
cp_airport.name
)
zone_color = {1: 0.0, 2: 1.0, 3: 0.5, 4: 0.15}
if cp_airport is None:
raise RuntimeError(
f"Could not find {airfield.airport.name} in the mission"
)
for runway in cp_airport.runways:
runway_end_1 = cp_airport.position.point_from_heading(
runway.heading, TRIGGER_RUNWAY_LENGTH_PRETENSE / 2
)
runway_end_2 = cp_airport.position.point_from_heading(
runway.heading + 180, TRIGGER_RUNWAY_LENGTH_PRETENSE / 2
)
runway_verticies = [
runway_end_1.point_from_heading(
runway.heading - 90, TRIGGER_RUNWAY_WIDTH_PRETENSE / 2
),
runway_end_1.point_from_heading(
runway.heading + 90, TRIGGER_RUNWAY_WIDTH_PRETENSE / 2
),
runway_end_2.point_from_heading(
runway.heading + 90, TRIGGER_RUNWAY_WIDTH_PRETENSE / 2
),
runway_end_2.point_from_heading(
runway.heading - 90, TRIGGER_RUNWAY_WIDTH_PRETENSE / 2
),
]
trigger_zone = self.mission.triggers.add_triggerzone_quad(
cp_airport.position,
runway_verticies,
hidden=False,
name=f"{cp_name_trimmed}-runway-{runway.id}",
color=zone_color,
)
break
def generate(self) -> None:
player_coalition = "blue"
enemy_coalition = "red"
self._set_skill(player_coalition, enemy_coalition)
self._set_allegiances(player_coalition, enemy_coalition)
self._generate_pretense_zone_triggers()
self._generate_capture_triggers(player_coalition, enemy_coalition)
@classmethod
def get_capture_zone_flag(cls) -> int:
flag = cls.capture_zone_flag
cls.capture_zone_flag += 1
return flag

View File

@ -416,15 +416,19 @@ class RadioRegistry:
already allocated.
"""
try:
while_count = 0
while (channel := random_frequency(radio)) in self.allocated_channels:
while_count += 1
if while_count > 1000:
raise StopIteration
pass
self.reserve(channel)
return channel
except StopIteration:
# In the event of too many channel users, fail gracefully by reusing
# the last channel.
# a channel.
# https://github.com/dcs-liberation/dcs_liberation/issues/598
channel = radio.last_channel
channel = random_frequency(radio)
logging.warning(
f"No more free channels for {radio.name}. Reusing {channel}."
)

View File

@ -49,6 +49,8 @@ FLIGHT_PLANNER_AUTOMATION = "Flight Planner Automation"
CAMPAIGN_DOCTRINE_PAGE = "Campaign Doctrine"
DOCTRINE_DISTANCES_SECTION = "Doctrine distances"
PRETENSE_PAGE = "Pretense"
MISSION_GENERATOR_PAGE = "Mission Generator"
GAMEPLAY_SECTION = "Gameplay"
@ -156,6 +158,7 @@ class Settings:
MISSION_RESTRICTIONS_SECTION,
default=True,
)
easy_communication: Optional[bool] = choices_option(
"Easy Communication",
page=DIFFICULTY_PAGE,
@ -174,6 +177,20 @@ class Settings:
# Campaign management
# General
squadron_random_chance: int = bounded_int_option(
"Percentage of randomly selected aircraft types (only for generated squadrons)",
page=CAMPAIGN_MANAGEMENT_PAGE,
section=GENERAL_SECTION,
default=50,
min=0,
max=100,
detail=(
"<p>Aircraft type selection is governed by the campaign and the squadron definitions available to "
"Retribution. Squadrons are generated by Retribution if the faction does not have access to the campaign "
"designer's squadron/aircraft definitions. Use the above to increase/decrease aircraft variety by making "
"some selections random instead of picking aircraft types from a priority list.</p>"
),
)
restrict_weapons_by_date: bool = boolean_option(
"Restrict weapons by date (WIP)",
page=CAMPAIGN_MANAGEMENT_PAGE,
@ -906,6 +923,17 @@ class Settings:
"Needed to cold-start some aircraft types. Might have a performance impact."
),
)
ground_start_airbase_statics_farps_remove: bool = boolean_option(
"Remove ground spawn statics, including invisible FARPs, at airbases",
MISSION_GENERATOR_PAGE,
GAMEPLAY_SECTION,
default=True,
detail=(
"Ammo and fuel statics and invisible FARPs should be unnecessary when creating "
"additional spawns for players at airbases. This setting will disable them and "
"potentially grant a marginal performance benefit."
),
)
ai_unlimited_fuel: bool = boolean_option(
"AI flights have unlimited fuel",
MISSION_GENERATOR_PAGE,
@ -1085,6 +1113,140 @@ class Settings:
"if the start-up type was manually changed to 'In-Flight'."
),
)
pretense_maxdistfromfront_distance: int = bounded_int_option(
"Max distance from front (km)",
page=PRETENSE_PAGE,
section=GENERAL_SECTION,
default=130,
min=10,
max=10000,
detail=(
"Zones farther away than this from the front line are switched "
"into low activity state, but will still be there as functional "
"parts of the economy. Use this to adjust performance."
),
)
pretense_controllable_carrier: bool = boolean_option(
"Controllable carrier",
page=PRETENSE_PAGE,
section=GENERAL_SECTION,
default=True,
detail=(
"This can be used to enable or disable the native carrier support in Pretense. The Pretense carrier "
"can be controlled through the communication menu (if the Pretense character has enough rank/CMD points) "
"and the player can call in AI aerial and cruise missile missions using it."
"The controllable carriers in Pretense do not build and deploy AI missions autonomously, so if you prefer "
"to have both sides deploy carrier aviation autonomously, you might want to disable this option. "
"When this option is disabled, moving the carrier can only be done with the Retribution interface."
),
)
pretense_carrier_steams_into_wind: bool = boolean_option(
"Carriers steam into wind",
page=PRETENSE_PAGE,
section=GENERAL_SECTION,
default=True,
detail=(
"This setting controls whether carriers and their escorts will steam into wind. Disable to "
"to ensure that the carriers stay within the carrier zone in Pretense, but note that "
"doing so might limit carrier operations, takeoff weights and landings."
),
)
pretense_carrier_zones_navmesh: str = choices_option(
"Navmesh to use for Pretense carrier zones",
page=PRETENSE_PAGE,
section=GENERAL_SECTION,
choices=["Blue navmesh", "Red navmesh"],
default="Blue navmesh",
detail=(
"Use the Retribution map interface options to compare the blue navmesh and the red navmesh."
"You can select which navmesh to use when generating the zones in which the controllable carrier(s) "
"move and operate."
),
)
pretense_extra_zone_connections: int = bounded_int_option(
"Extra friendly zone connections",
page=PRETENSE_PAGE,
section=GENERAL_SECTION,
default=2,
min=0,
max=10,
detail=(
"Add connections from each zone to this many closest friendly zones,"
"which don't have an existing supply route defined in the campaign."
),
)
pretense_num_of_cargo_planes: int = bounded_int_option(
"Number of cargo planes per side",
page=PRETENSE_PAGE,
section=GENERAL_SECTION,
default=2,
min=1,
max=100,
)
pretense_sead_flights_per_cp: int = bounded_int_option(
"Number of AI SEAD flights per control point / zone",
page=PRETENSE_PAGE,
section=GENERAL_SECTION,
default=1,
min=1,
max=10,
)
pretense_cas_flights_per_cp: int = bounded_int_option(
"Number of AI CAS flights per control point / zone",
page=PRETENSE_PAGE,
section=GENERAL_SECTION,
default=1,
min=1,
max=10,
)
pretense_bai_flights_per_cp: int = bounded_int_option(
"Number of AI BAI flights per control point / zone",
page=PRETENSE_PAGE,
section=GENERAL_SECTION,
default=1,
min=1,
max=10,
)
pretense_strike_flights_per_cp: int = bounded_int_option(
"Number of AI Strike flights per control point / zone",
page=PRETENSE_PAGE,
section=GENERAL_SECTION,
default=1,
min=1,
max=10,
)
pretense_barcap_flights_per_cp: int = bounded_int_option(
"Number of AI BARCAP flights per control point / zone",
page=PRETENSE_PAGE,
section=GENERAL_SECTION,
default=1,
min=1,
max=10,
)
pretense_ai_aircraft_per_flight: int = bounded_int_option(
"Number of AI aircraft per flight",
page=PRETENSE_PAGE,
section=GENERAL_SECTION,
default=2,
min=1,
max=4,
)
pretense_player_flights_per_type: int = bounded_int_option(
"Number of player flights per aircraft type at each base",
page=PRETENSE_PAGE,
section=GENERAL_SECTION,
default=1,
min=1,
max=10,
)
pretense_ai_cargo_planes_per_side: int = bounded_int_option(
"Number of AI cargo planes per side",
page=PRETENSE_PAGE,
section=GENERAL_SECTION,
default=2,
min=1,
max=20,
)
# Cheating. Not using auto settings because the same page also has buttons which do
# not alter settings.

View File

@ -202,6 +202,29 @@ class ConflictTheater:
assert closest_red is not None
return closest_blue, closest_red
def closest_friendly_control_points_to(
self, cp: ControlPoint
) -> List[ControlPoint]:
"""
Returns a list of the friendly ControlPoints in theater to ControlPoint cp, sorted closest to farthest.
"""
closest_cps = list()
distances_to_cp = dict()
if cp.captured:
control_points = self.player_points()
else:
control_points = self.enemy_points()
for other_cp in control_points:
if cp == other_cp:
continue
dist = other_cp.position.distance_to_point(cp.position)
distances_to_cp[dist] = other_cp
for i in sorted(distances_to_cp.keys()):
closest_cps.append(distances_to_cp[i])
return closest_cps
def find_control_point_by_id(self, cp_id: UUID) -> ControlPoint:
for i in self.controlpoints:
if i.id == cp_id:

View File

@ -32,6 +32,9 @@ def load_icons():
"./resources/ui/misc/" + get_theme_icons() + "/github.png"
)
ICONS["Ukraine"] = QPixmap("./resources/ui/misc/ukraine.png")
ICONS["Pretense"] = QPixmap("./resources/ui/misc/pretense.png")
ICONS["Pretense_discord"] = QPixmap("./resources/ui/misc/pretense_discord.png")
ICONS["Pretense_generate"] = QPixmap("./resources/ui/misc/pretense_generate.png")
ICONS["Control Points"] = QPixmap(
"./resources/ui/misc/" + get_theme_icons() + "/circle.png"

View File

@ -1,6 +1,7 @@
import logging
import traceback
import webbrowser
from datetime import datetime
from pathlib import Path
from typing import Optional
@ -21,6 +22,8 @@ from game import Game, VERSION, persistency, Migrator
from game.debriefing import Debriefing
from game.game import TurnState
from game.layout import LAYOUTS
from game.persistency import pre_pretense_backups_dir
from game.pretense.pretensemissiongenerator import PretenseMissionGenerator
from game.server import EventStream, GameContext
from game.server.dependencies import QtCallbacks, QtContext
from game.theater import ControlPoint, MissionTarget, TheaterGroundObject
@ -193,6 +196,20 @@ class QLiberationWindow(QMainWindow):
lambda: webbrowser.open_new_tab("https://shdwp.github.io/ukraine/")
)
self.pretenseLinkAction = QAction("&DCS: Pretense", self)
self.pretenseLinkAction.setIcon(QIcon(CONST.ICONS["Pretense_discord"]))
self.pretenseLinkAction.triggered.connect(
lambda: webbrowser.open_new_tab(
"https://" + "discord.gg" + "/" + "PtPsb9Mpk6"
)
)
self.newPretenseAction = QAction(
"&Generate a Pretense Campaign from the running campaign", self
)
self.newPretenseAction.setIcon(QIcon(CONST.ICONS["Pretense_generate"]))
self.newPretenseAction.triggered.connect(self.newPretenseCampaign)
self.openLogsAction = QAction("Show &logs", self)
self.openLogsAction.triggered.connect(self.showLogsDialog)
@ -234,6 +251,8 @@ class QLiberationWindow(QMainWindow):
self.links_bar.addAction(self.openDiscordAction)
self.links_bar.addAction(self.openGithubAction)
self.links_bar.addAction(self.ukraineAction)
self.links_bar.addAction(self.pretenseLinkAction)
self.links_bar.addAction(self.newPretenseAction)
self.actions_bar = self.addToolBar("Actions")
self.actions_bar.addAction(self.openSettingsAction)
@ -303,6 +322,29 @@ class QLiberationWindow(QMainWindow):
wizard.show()
wizard.accepted.connect(lambda: self.onGameGenerated(wizard.generatedGame))
def newPretenseCampaign(self):
output = persistency.mission_path_for("pretense_campaign.miz")
try:
PretenseMissionGenerator(
self.game, self.game.conditions.start_time
).generate_miz(output)
except Exception as e:
now = datetime.now()
date_time = now.strftime("%Y-%d-%mT%H_%M_%S")
path = pre_pretense_backups_dir()
path.mkdir(parents=True, exist_ok=True)
tgt = path / f"pre-pretense-backup_{date_time}.retribution"
path /= f".pre-pretense-backup.retribution"
if path.exists():
with open(path, "rb") as source:
with open(tgt, "wb") as target:
target.write(source.read())
raise e
title = "Pretense campaign generated"
msg = f"A Pretense campaign mission has been successfully generated in {output}"
QMessageBox.information(QApplication.focusWidget(), title, msg, QMessageBox.Ok)
def openFile(self):
if self.game is not None and self.game.savepath:
save_dir = self.game.savepath

Binary file not shown.

View File

@ -0,0 +1,104 @@
---
name: Afghanistan - [Pretense] - Full map
theater: Afghanistan
authors: Colonel Akir Nakesh
recommended_player_faction: Bluefor Modern
recommended_enemy_faction: Insurgents
description:
<p>An Afghanistan full map campaign. The campaign is tuned for out-of-the-box Pretense generation compatibility and as such may be unbalanced for a standard campaign.</p>
miz: afghanistan_full.miz
performance: 3
version: "10.7"
squadrons:
# Herat
1:
- primary: Air Assault
aircraft:
- UH-1H Iroquois
- Mi-8MTV2 Hip
- primary: CAS
aircraft:
- AH-64D Apache Longbow
- Ka-50 Hokum III
- Ka-50 Hokum
- Mi-24P Hind-F
- primary: CAS
secondary: any
aircraft:
- A-10C Thunderbolt II (Suite 7)
- A-10C Thunderbolt II (Suite 3)
- A-10A Thunderbolt II
- Su-25 Frogfoot
- L-39ZA Albatros
- primary: SEAD
aircraft:
- F-16CM Fighting Falcon (Block 50)
- F/A-18C Hornet (Lot 20)
- Su-25T Frogfoot
- primary: BARCAP
secondary: air-to-air
aircraft:
- F-15C Eagle
- J-11A Flanker-L
- MiG-29S Fulcrum-C
- Su-27 Flanker-B
- Su-33 Flanker-D
- MiG-29A Fulcrum-A
- primary: AEW&C
aircraft:
- E-3A
- A-50
- primary: Refueling
aircraft:
- KC-135 Stratotanker
- IL-78M
- primary: Transport
aircraft:
- C-130
- An-26B
# Kandahar
7:
- primary: Air Assault
aircraft:
- UH-1H Iroquois
- Mi-8MTV2 Hip
- primary: CAS
aircraft:
- AH-64D Apache Longbow
- Ka-50 Hokum III
- Ka-50 Hokum
- Mi-24P Hind-F
- primary: CAS
secondary: any
aircraft:
- A-10C Thunderbolt II (Suite 7)
- A-10C Thunderbolt II (Suite 3)
- A-10A Thunderbolt II
- Su-25 Frogfoot
- L-39ZA Albatros
- primary: SEAD
aircraft:
- F-16CM Fighting Falcon (Block 50)
- F/A-18C Hornet (Lot 20)
- Su-25T Frogfoot
- primary: BARCAP
secondary: air-to-air
aircraft:
- F-15C Eagle
- J-11A Flanker-L
- MiG-29S Fulcrum-C
- Su-27 Flanker-B
- Su-33 Flanker-D
- MiG-29A Fulcrum-A
- primary: AEW&C
aircraft:
- E-3A
- A-50
- primary: Refueling
aircraft:
- KC-135 Stratotanker
- IL-78M
- primary: Transport
aircraft:
- C-130
- An-26B

Binary file not shown.

View File

@ -0,0 +1,126 @@
---
name: Marianas - [Pretense] - Full map
theater: MarianaIslands
authors: Colonel Akir Nakesh
recommended_player_faction: Bluefor Modern
recommended_enemy_faction: China 2010
description:
<p>A Marianas full map campaign. The campaign is tuned for out-of-the-box Pretense generation compatibility and as such will be unbalanced for a standard campaign.</p>
miz: marianas_full.miz
performance: 3
version: "10.7"
squadrons:
#Andersen
6:
- primary: Air Assault
aircraft:
- UH-1H Iroquois
- Mi-8MTV2 Hip
- primary: CAS
aircraft:
- AH-64D Apache Longbow
- Ka-50 Hokum III
- Ka-50 Hokum
- Mi-24P Hind-F
- primary: CAS
secondary: any
aircraft:
- A-10C Thunderbolt II (Suite 7)
- A-10C Thunderbolt II (Suite 3)
- A-10A Thunderbolt II
- Su-25 Frogfoot
- L-39ZA Albatros
- primary: SEAD
aircraft:
- F-16CM Fighting Falcon (Block 50)
- F/A-18C Hornet (Lot 20)
- Su-25T Frogfoot
- primary: BARCAP
secondary: air-to-air
aircraft:
- F-15C Eagle
- J-11A Flanker-L
- MiG-29S Fulcrum-C
- Su-27 Flanker-B
- Su-33 Flanker-D
- MiG-29A Fulcrum-A
- primary: AEW&C
aircraft:
- E-3A
- A-50
- primary: Refueling
aircraft:
- KC-135 Stratotanker
- IL-78M
- primary: Transport
aircraft:
- C-130
- An-26B
#Saipan
2:
- primary: Air Assault
aircraft:
- UH-1H Iroquois
- Mi-8MTV2 Hip
- primary: CAS
aircraft:
- AH-64D Apache Longbow
- Ka-50 Hokum III
- Ka-50 Hokum
- Mi-24P Hind-F
- primary: CAS
secondary: any
aircraft:
- A-10C Thunderbolt II (Suite 7)
- A-10C Thunderbolt II (Suite 3)
- A-10A Thunderbolt II
- Su-25 Frogfoot
- L-39ZA Albatros
- primary: SEAD
aircraft:
- F-16CM Fighting Falcon (Block 50)
- F/A-18C Hornet (Lot 20)
- Su-25T Frogfoot
- primary: BARCAP
secondary: air-to-air
aircraft:
- F-15C Eagle
- J-11A Flanker-L
- MiG-29S Fulcrum-C
- Su-27 Flanker-B
- Su-33 Flanker-D
- MiG-29A Fulcrum-A
- primary: AEW&C
aircraft:
- E-3A
- A-50
- primary: Refueling
aircraft:
- KC-135 Stratotanker
- IL-78M
- primary: Transport
aircraft:
- C-130
- An-26B
Blue CVBG:
- primary: BARCAP
aircraft:
- F/A-18C Hornet (Lot 20)
- Su-33 Flanker-D
- primary: AEW&C
aircraft:
- E-2D Advanced Hawkeye
- primary: Refueling
aircraft:
- S-3B Tanker
Red CVBG:
- primary: BARCAP
aircraft:
- F/A-18C Hornet (Lot 20)
- Su-33 Flanker-D
- primary: AEW&C
aircraft:
- E-2D Advanced Hawkeye
- primary: Refueling
aircraft:
- S-3B Tanker

Binary file not shown.

View File

@ -0,0 +1,104 @@
---
name: Nevada - [Pretense] - Full map
theater: Nevada
authors: Colonel Akir Nakesh
recommended_player_faction: Bluefor Modern
recommended_enemy_faction: USAF Aggressors
description:
<p>A Nevada full map campaign. The campaign is tuned for out-of-the-box Pretense generation compatibility and as such may be unbalanced for a standard campaign.</p>
miz: nevada_full.miz
performance: 3
version: "10.7"
squadrons:
# Nellis
4:
- primary: Air Assault
aircraft:
- UH-1H Iroquois
- Mi-8MTV2 Hip
- primary: CAS
aircraft:
- AH-64D Apache Longbow
- Ka-50 Hokum III
- Ka-50 Hokum
- Mi-24P Hind-F
- primary: CAS
secondary: any
aircraft:
- A-10C Thunderbolt II (Suite 7)
- A-10C Thunderbolt II (Suite 3)
- A-10A Thunderbolt II
- Su-25 Frogfoot
- L-39ZA Albatros
- primary: SEAD
aircraft:
- F-16CM Fighting Falcon (Block 50)
- F/A-18C Hornet (Lot 20)
- Su-25T Frogfoot
- primary: BARCAP
secondary: air-to-air
aircraft:
- F-15C Eagle
- J-11A Flanker-L
- MiG-29S Fulcrum-C
- Su-27 Flanker-B
- Su-33 Flanker-D
- MiG-29A Fulcrum-A
- primary: AEW&C
aircraft:
- E-3A
- A-50
- primary: Refueling
aircraft:
- KC-135 Stratotanker
- IL-78M
- primary: Transport
aircraft:
- C-130
- An-26B
# Tonopah Test Range
18:
- primary: Air Assault
aircraft:
- UH-1H Iroquois
- Mi-8MTV2 Hip
- primary: CAS
aircraft:
- AH-64D Apache Longbow
- Ka-50 Hokum III
- Ka-50 Hokum
- Mi-24P Hind-F
- primary: CAS
secondary: any
aircraft:
- A-10C Thunderbolt II (Suite 7)
- A-10C Thunderbolt II (Suite 3)
- A-10A Thunderbolt II
- Su-25 Frogfoot
- L-39ZA Albatros
- primary: SEAD
aircraft:
- F-16CM Fighting Falcon (Block 50)
- F/A-18C Hornet (Lot 20)
- Su-25T Frogfoot
- primary: BARCAP
secondary: air-to-air
aircraft:
- F-15C Eagle
- J-11A Flanker-L
- MiG-29S Fulcrum-C
- Su-27 Flanker-B
- Su-33 Flanker-D
- MiG-29A Fulcrum-A
- primary: AEW&C
aircraft:
- E-3A
- A-50
- primary: Refueling
aircraft:
- KC-135 Stratotanker
- IL-78M
- primary: Transport
aircraft:
- C-130
- An-26B

Binary file not shown.

View File

@ -0,0 +1,132 @@
---
name: Persian Gulf - [Pretense] - Full map
theater: Persian Gulf
authors: Colonel Akir Nakesh
recommended_player_faction: Bluefor Modern
recommended_enemy_faction: Iran 2015
description:
<p>A Persian Gulf full map campaign. The campaign is tuned for out-of-the-box Pretense generation compatibility and as such will be unbalanced for a standard campaign. Recommended settings for campaign generation&#58; disable frontline smoke, disable CTLD, disable Skynet, set CVBG navmesh to Redfor.</p>
miz: persian_gulf_full.miz
performance: 3
version: "10.7"
settings:
pretense_carrier_zones_navmesh: "Red navmesh"
perf_smoke_gen: false
plugins:
ctld: false
skynetiads: false
squadrons:
#Al Dhafra AFB
4:
- primary: Air Assault
aircraft:
- UH-1H Iroquois
- Mi-8MTV2 Hip
- primary: CAS
aircraft:
- AH-64D Apache Longbow
- Ka-50 Hokum III
- Ka-50 Hokum
- Mi-24P Hind-F
- primary: CAS
secondary: any
aircraft:
- A-10C Thunderbolt II (Suite 7)
- A-10C Thunderbolt II (Suite 3)
- A-10A Thunderbolt II
- Su-25 Frogfoot
- L-39ZA Albatros
- primary: SEAD
aircraft:
- F-16CM Fighting Falcon (Block 50)
- F/A-18C Hornet (Lot 20)
- Su-25T Frogfoot
- primary: BARCAP
secondary: air-to-air
aircraft:
- F-15C Eagle
- J-11A Flanker-L
- MiG-29S Fulcrum-C
- Su-27 Flanker-B
- Su-33 Flanker-D
- MiG-29A Fulcrum-A
- primary: AEW&C
aircraft:
- E-3A
- A-50
- primary: Refueling
aircraft:
- KC-135 Stratotanker
- IL-78M
- primary: Transport
aircraft:
- C-130
- An-26B
#Shiraz Intl
19:
- primary: Air Assault
aircraft:
- UH-1H Iroquois
- Mi-8MTV2 Hip
- primary: CAS
aircraft:
- AH-64D Apache Longbow
- Ka-50 Hokum III
- Ka-50 Hokum
- Mi-24P Hind-F
- primary: CAS
secondary: any
aircraft:
- A-10C Thunderbolt II (Suite 7)
- A-10C Thunderbolt II (Suite 3)
- A-10A Thunderbolt II
- Su-25 Frogfoot
- L-39ZA Albatros
- primary: SEAD
aircraft:
- F-16CM Fighting Falcon (Block 50)
- F/A-18C Hornet (Lot 20)
- Su-25T Frogfoot
- primary: BARCAP
secondary: air-to-air
aircraft:
- F-15C Eagle
- J-11A Flanker-L
- MiG-29S Fulcrum-C
- Su-27 Flanker-B
- Su-33 Flanker-D
- MiG-29A Fulcrum-A
- primary: AEW&C
aircraft:
- E-3A
- A-50
- primary: Refueling
aircraft:
- KC-135 Stratotanker
- IL-78M
- primary: Transport
aircraft:
- C-130
- An-26B
Blue CVBG:
- primary: BARCAP
aircraft:
- F/A-18C Hornet (Lot 20)
- Su-33 Flanker-D
- primary: AEW&C
aircraft:
- E-2D Advanced Hawkeye
- primary: Refueling
aircraft:
- S-3B Tanker
Red CVBG:
- primary: BARCAP
aircraft:
- F/A-18C Hornet (Lot 20)
- Su-33 Flanker-D
- primary: AEW&C
aircraft:
- E-2D Advanced Hawkeye
- primary: Refueling
aircraft:
- S-3B Tanker

View File

@ -0,0 +1,519 @@
end
presets = {
upgrades = {
basic = {
tent = Preset:new({
display = 'Tent',
cost = 1500,
type = 'upgrade',
template = "tent"
}),
comPost = Preset:new({
display = 'Barracks',
cost = 1500,
type = 'upgrade',
template = "barracks"
}),
outpost = Preset:new({
display = 'Outpost',
cost = 1500,
type = 'upgrade',
template = "outpost"
}),
artyBunker = Preset:new({
display = 'Artillery Bunker',
cost = 2000,
type = 'upgrade',
template = "ammo-depot"
})
},
attack = {
ammoCache = Preset:new({
display = 'Ammo Cache',
cost = 1500,
type = 'upgrade',
template = "ammo-cache"
}),
ammoDepot = Preset:new({
display = 'Ammo Depot',
cost = 2000,
type = 'upgrade',
template = "ammo-depot"
}),
chemTank = Preset:new({
display='Chemical Tank',
cost = 2000,
type ='upgrade',
template = "chem-tank"
}),
},
supply = {
fuelCache = Preset:new({
display = 'Fuel Cache',
cost = 1500,
type = 'upgrade',
template = "fuel-cache"
}),
fuelTank = Preset:new({
display = 'Fuel Tank',
cost = 1500,
type = 'upgrade',
template = "fuel-tank-big"
}),
fuelTankFarp = Preset:new({
display = 'Fuel Tank',
cost = 1500,
type = 'upgrade',
template = "fuel-tank-small"
}),
factory1 = Preset:new({
display='Factory',
cost = 2000,
type ='upgrade',
income = 20,
template = "factory-1"
}),
factory2 = Preset:new({
display='Factory',
cost = 2000,
type ='upgrade',
income = 20,
template = "factory-2"
}),
factoryTank = Preset:new({
display='Storage Tank',
cost = 1500,
type ='upgrade',
income = 10,
template = "chem-tank"
}),
ammoDepot = Preset:new({
display = 'Ammo Depot',
cost = 2000,
type = 'upgrade',
income = 40,
template = "ammo-depot"
}),
oilPump = Preset:new({
display = 'Oil Pump',
cost = 1500,
type = 'upgrade',
income = 20,
template = "oil-pump"
}),
hangar = Preset:new({
display = 'Hangar',
cost = 2000,
type = 'upgrade',
income = 30,
template = "hangar"
}),
excavator = Preset:new({
display = 'Excavator',
cost = 2000,
type = 'upgrade',
income = 20,
template = "excavator"
}),
farm1 = Preset:new({
display = 'Farm House',
cost = 2000,
type = 'upgrade',
income = 40,
template = "farm-house-1"
}),
farm2 = Preset:new({
display = 'Farm House',
cost = 2000,
type = 'upgrade',
income = 40,
template = "farm-house-2"
}),
refinery1 = Preset:new({
display='Refinery',
cost = 2000,
type ='upgrade',
income = 100,
template = "factory-1"
}),
powerplant1 = Preset:new({
display='Power Plant',
cost = 1500,
type ='upgrade',
income = 25,
template = "factory-1"
}),
powerplant2 = Preset:new({
display='Power Plant',
cost = 1500,
type ='upgrade',
income = 25,
template = "factory-2"
}),
antenna = Preset:new({
display='Antenna',
cost = 1000,
type ='upgrade',
income = 10,
template = "antenna"
}),
hq = Preset:new({
display='HQ Building',
cost = 2000,
type ='upgrade',
income = 50,
template = "tv-tower"
}),
},
airdef = {
bunker = Preset:new({
display = 'Excavator',
cost = 1500,
type = 'upgrade',
template = "excavator"
}),
comCenter = Preset:new({
display = 'Command Center',
cost = 12500,
type = 'upgrade',
template = "command-center"
})
}
},
defenses = {
red = {
infantry = Preset:new({
display = 'Infantry',
cost=2000,
type='defense',
template='infantry-red',
}),
artillery = Preset:new({
display = 'Artillery',
cost=2500,
type='defense',
template='artillery-red',
}),
shorad = Preset:new({
display = 'SHORAD',
cost=2500,
type='defense',
template='shorad-red',
}),
sa2 = Preset:new({
display = 'SAM',
cost=3000,
type='defense',
template='sa2-red',
}),
sa10 = Preset:new({
display = 'SAM',
cost=30000,
type='defense',
template='sa10-red',
}),
sa5 = Preset:new({
display = 'SAM',
cost=20000,
type='defense',
template='sa5-red',
}),
sa3 = Preset:new({
display = 'SAM',
cost=4000,
type='defense',
template='sa3-red',
}),
sa6 = Preset:new({
display = 'SAM',
cost=6000,
type='defense',
template='sa6-red',
}),
sa11 = Preset:new({
display = 'SAM',
cost=10000,
type='defense',
template='sa11-red',
}),
hawk = Preset:new({
display = 'SAM',
cost=6000,
type='defense',
template='hawk-red',
}),
patriot = Preset:new({
display = 'SAM',
cost=30000,
type='defense',
template='patriot-red',
}),
nasamsb = Preset:new({
display = 'SAM',
cost=3000,
type='defense',
template='nasamsb-red',
}),
nasamsc = Preset:new({
display = 'SAM',
cost=3000,
type='defense',
template='nasamsc-red',
}),
rapier = Preset:new({
display = 'SAM',
cost=3000,
type='defense',
template='rapier-red',
}),
roland = Preset:new({
display = 'SAM',
cost=3000,
type='defense',
template='roland-red',
}),
irondome = Preset:new({
display = 'SAM',
cost=20000,
type='defense',
template='irondome-red',
}),
davidsling = Preset:new({
display = 'SAM',
cost=30000,
type='defense',
template='davidsling-red',
}),
hq7 = Preset:new({
display = 'SAM',
cost=3000,
type='defense',
template='hq7-red',
})
},
blue = {
infantry = Preset:new({
display = 'Infantry',
cost=2000,
type='defense',
template='infantry-blue',
}),
artillery = Preset:new({
display = 'Artillery',
cost=2500,
type='defense',
template='artillery-blue',
}),
shorad = Preset:new({
display = 'SHORAD',
cost=2500,
type='defense',
template='shorad-blue',
}),
sa2 = Preset:new({
display = 'SAM',
cost=3000,
type='defense',
template='sa2-blue',
}),
sa10 = Preset:new({
display = 'SAM',
cost=30000,
type='defense',
template='sa10-blue',
}),
sa5 = Preset:new({
display = 'SAM',
cost=20000,
type='defense',
template='sa5-blue',
}),
sa3 = Preset:new({
display = 'SAM',
cost=4000,
type='defense',
template='sa3-blue',
}),
sa6 = Preset:new({
display = 'SAM',
cost=6000,
type='defense',
template='sa6-blue',
}),
sa11 = Preset:new({
display = 'SAM',
cost=10000,
type='defense',
template='sa11-blue',
}),
hawk = Preset:new({
display = 'SAM',
cost=6000,
type='defense',
template='hawk-blue',
}),
patriot = Preset:new({
display = 'SAM',
cost=30000,
type='defense',
template='patriot-blue',
}),
nasamsb = Preset:new({
display = 'SAM',
cost=3000,
type='defense',
template='nasamsb-blue',
}),
nasamsc = Preset:new({
display = 'SAM',
cost=3000,
type='defense',
template='nasamsc-blue',
}),
rapier = Preset:new({
display = 'SAM',
cost=3000,
type='defense',
template='rapier-blue',
}),
roland = Preset:new({
display = 'SAM',
cost=3000,
type='defense',
template='roland-blue',
}),
irondome = Preset:new({
display = 'SAM',
cost=20000,
type='defense',
template='irondome-blue',
}),
davidsling = Preset:new({
display = 'SAM',
cost=30000,
type='defense',
template='davidsling-blue',
}),
hq7 = Preset:new({
display = 'SAM',
cost=3000,
type='defense',
template='hq7-blue',
})
}
},
missions = {
supply = {
convoy = Preset:new({
display = 'Supply convoy',
cost = 4000,
type = 'mission',
missionType = ZoneCommand.missionTypes.supply_convoy
}),
convoy_escorted = Preset:new({
display = 'Supply convoy',
cost = 3000,
type = 'mission',
missionType = ZoneCommand.missionTypes.supply_convoy
}),
helo = Preset:new({
display = 'Supply helicopter',
cost = 2500,
type='mission',
missionType = ZoneCommand.missionTypes.supply_air
}),
transfer = Preset:new({
display = 'Supply transfer',
cost = 1000,
type='mission',
missionType = ZoneCommand.missionTypes.supply_transfer
})
},
attack = {
surface = Preset:new({
display = 'Ground assault',
cost = 100,
type = 'mission',
missionType = ZoneCommand.missionTypes.assault,
}),
cas = Preset:new({
display = 'CAS',
cost = 200,
type='mission',
missionType = ZoneCommand.missionTypes.cas
}),
bai = Preset:new({
display = 'BAI',
cost = 200,
type='mission',
missionType = ZoneCommand.missionTypes.bai
}),
strike = Preset:new({
display = 'Strike',
cost = 300,
type='mission',
missionType = ZoneCommand.missionTypes.strike
}),
sead = Preset:new({
display = 'SEAD',
cost = 200,
type='mission',
missionType = ZoneCommand.missionTypes.sead
}),
helo = Preset:new({
display = 'CAS',
cost = 100,
type='mission',
missionType = ZoneCommand.missionTypes.cas_helo
})
},
patrol={
aircraft = Preset:new({
display= "Patrol",
cost = 100,
type='mission',
missionType = ZoneCommand.missionTypes.patrol
})
},
support ={
awacs = Preset:new({
display= "AWACS",
cost = 300,
type='mission',
bias='5',
missionType = ZoneCommand.missionTypes.awacs
}),
tanker = Preset:new({
display= "Tanker",
cost = 200,
type='mission',
bias='2',
missionType = ZoneCommand.missionTypes.tanker
})
}
},
special = {
red = {
infantry = Preset:new({
display = 'Infantry',
cost=-1,
type='defense',
template='defense-red',
}),
},
blue = {
infantry = Preset:new({
display = 'Infantry',
cost=-1,
type='defense',
template='defense-blue',
})
}
}
}
zones = {}
do

View File

@ -0,0 +1,32 @@
end
ZoneCommand.setNeighbours(cm)
bm = BattlefieldManager:new()
mc = MarkerCommands:new()
pt = PlayerTracker:new(mc)
mt = MissionTracker:new(pt, mc)
st = SquadTracker:new()
ct = CSARTracker:new()
pl = PlayerLogistics:new(mt, pt, st, ct)
gci = GCI:new(2)
gm = GroupMonitor:new(cm)
ZoneCommand.groupMonitor = gm
-- PlayerLogistics:registerSquadGroup(squadType, groupname, weight,cost,jobtime,extracttime, squadSize)
pl:registerSquadGroup(PlayerLogistics.infantryTypes.capture, 'capture-squad', 700, 200, 60, 60*30, 4)
pl:registerSquadGroup(PlayerLogistics.infantryTypes.sabotage, 'sabotage-squad', 800, 500, 60*5, 60*30, 4)
pl:registerSquadGroup(PlayerLogistics.infantryTypes.ambush, 'ambush-squad', 900, 300, 60*20, 60*30, 5)
pl:registerSquadGroup(PlayerLogistics.infantryTypes.engineer, 'engineer-squad', 200, 1000,60, 60*30, 2)
pl:registerSquadGroup(PlayerLogistics.infantryTypes.manpads, 'manpads-squad', 900, 500, 60*20, 60*30, 5)
pl:registerSquadGroup(PlayerLogistics.infantryTypes.spy, 'spy-squad', 100, 300, 60*10, 60*30, 1)
pl:registerSquadGroup(PlayerLogistics.infantryTypes.rapier, 'rapier-squad', 1200,2000,60*60, 60*30, 8)

View File

@ -0,0 +1,62 @@
pm = PersistenceManager:new(savefile, gm, st, ct, pl)
pm:load()
if pm:canRestore() then
pm:restoreZones()
pm:restoreAIMissions()
pm:restoreBattlefield()
pm:restoreCsar()
pm:restoreSquads()
else
--initial states
Starter.start(zones)
end
timer.scheduleFunction(function(param, time)
pm:save()
env.info("Mission state saved")
return time+60
end, zones, timer.getTime()+60)
--make sure support units are present where needed
ensureSpawn = {
['golf-farp-suport'] = zones.golf,
['november-farp-suport'] = zones.november,
['tango-farp-suport'] = zones.tango,
['sierra-farp-suport'] = zones.sierra,
['cherkessk-farp-suport'] = zones.cherkessk,
['unal-farp-suport'] = zones.unal,
['tyrnyauz-farp-suport'] = zones.tyrnyauz
}
for grname, zn in pairs(ensureSpawn) do
local g = Group.getByName(grname)
if g then g:destroy() end
end
timer.scheduleFunction(function(param, time)
for grname, zn in pairs(ensureSpawn) do
local g = Group.getByName(grname)
if zn.side == 2 then
if not g then
local err, msg = pcall(mist.respawnGroup,grname,true)
if not err then
env.info("ERROR spawning "..grname)
env.info(msg)
end
end
else
if g then g:destroy() end
end
end
return time+30
end, {}, timer.getTime()+30)
--supply injection

View File

@ -0,0 +1,141 @@
supplyPointRegistry = {
blue = {},
red = {}
}
for i,v in ipairs(blueSupply) do
local g = Group.getByName(v)
if g then
supplyPointRegistry.blue[v] = g:getUnit(1):getPoint()
end
end
for i,v in ipairs(redSupply) do
local g = Group.getByName(v)
if g then
supplyPointRegistry.red[v] = g:getUnit(1):getPoint()
end
end
offmapSupplyRegistry = {}
timer.scheduleFunction(function(param, time)
local availableBlue = {}
for i,v in ipairs(param.blue) do
if offmapSupplyRegistry[v] == nil then
table.insert(availableBlue, v)
end
end
local availableRed = {}
for i,v in ipairs(param.red) do
if offmapSupplyRegistry[v] == nil then
table.insert(availableRed, v)
end
end
local redtargets = {}
local bluetargets = {}
for _, zn in ipairs(param.offmapZones) do
if zn:needsSupplies(3000) then
local isOnRoute = false
for _,data in pairs(offmapSupplyRegistry) do
if data.zone.name == zn.name then
isOnRoute = true
break
end
end
if not isOnRoute then
if zn.side == 1 then
table.insert(redtargets, zn)
elseif zn.side == 2 then
table.insert(bluetargets, zn)
end
end
end
end
if #availableRed > 0 and #redtargets > 0 then
local zn = redtargets[math.random(1,#redtargets)]
local red = nil
local minD = 999999999
for i,v in ipairs(availableRed) do
local d = mist.utils.get2DDist(zn.zone.point, supplyPointRegistry.red[v])
if d < minD then
red = v
minD = d
end
end
if not red then red = availableRed[math.random(1,#availableRed)] end
local gr = red
red = nil
mist.respawnGroup(gr, true)
offmapSupplyRegistry[gr] = {zone = zn, assigned = timer.getAbsTime()}
env.info(gr..' was deployed')
timer.scheduleFunction(function(param,time)
local g = Group.getByName(param.group)
TaskExtensions.landAtAirfield(g, param.target.zone.point)
env.info(param.group..' going to '..param.target.name)
end, {group=gr, target=zn}, timer.getTime()+2)
end
if #availableBlue > 0 and #bluetargets>0 then
local zn = bluetargets[math.random(1,#bluetargets)]
local blue = nil
local minD = 999999999
for i,v in ipairs(availableBlue) do
local d = mist.utils.get2DDist(zn.zone.point, supplyPointRegistry.blue[v])
if d < minD then
blue = v
minD = d
end
end
if not blue then blue = availableBlue[math.random(1,#availableBlue)] end
local gr = blue
blue = nil
mist.respawnGroup(gr, true)
offmapSupplyRegistry[gr] = {zone = zn, assigned = timer.getAbsTime()}
env.info(gr..' was deployed')
timer.scheduleFunction(function(param,time)
local g = Group.getByName(param.group)
TaskExtensions.landAtAirfield(g, param.target.zone.point)
env.info(param.group..' going to '..param.target.name)
end, {group=gr, target=zn}, timer.getTime()+2)
end
return time+(60*5)
end, {blue = blueSupply, red = redSupply, offmapZones = offmapZones}, timer.getTime()+60)
timer.scheduleFunction(function(param, time)
for groupname,data in pairs(offmapSupplyRegistry) do
local gr = Group.getByName(groupname)
if not gr then
offmapSupplyRegistry[groupname] = nil
env.info(groupname..' was destroyed')
end
if gr and ((timer.getAbsTime() - data.assigned) > (60*60)) then
gr:destroy()
offmapSupplyRegistry[groupname] = nil
env.info(groupname..' despawned due to being alive for too long')
end
if gr and Utils.allGroupIsLanded(gr) and Utils.someOfGroupInZone(gr, data.zone.name) then
data.zone:addResource(15000)
gr:destroy()
offmapSupplyRegistry[groupname] = nil
env.info(groupname..' landed at '..data.zone.name..' and delivered 15000 resources')
end
end
return time+180
end, {}, timer.getTime()+180)

View File

@ -0,0 +1,12 @@
if lfs then
local dir = lfs.writedir()..'Missions/Saves/'
lfs.mkdir(dir)
savefile = dir..savefile
env.info('Pretense - Save file path: '..savefile)
end
do

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,4 +1,5 @@
class: Logistics
price: 2
variants:
LARC-V: null
class: Logistics
price: 3
variants:
LARC-V Amphibious Cargo Vehicle: null
LARC-V: null