Move mission generation code into game.

Operation has been renamed MissionGenerator and is no longer a static
class.
This commit is contained in:
Dan Albert 2021-10-22 13:31:43 -07:00
parent b0787d9a3f
commit 74291271e3
43 changed files with 805 additions and 774 deletions

View File

@ -16,7 +16,7 @@ class FlightType(Enum):
* flightplan.py: Add waypoint population in generate_flight_plan. Add a new flight
plan type if necessary, though most are a subclass of StrikeFlightPlan.
* aircraft.py: Add a configuration method and call it in setup_flight_group. This is
* aircraftgenerator.py: Add a configuration method and call it in setup_flight_group. This is
responsible for configuring waypoint 0 actions like setting ROE, threat reaction,
and mission abort parameters (winchester, bingo, etc).
* Implementations of MissionTarget.mission_types: A mission type can only be planned
@ -28,7 +28,7 @@ class FlightType(Enum):
You may also need to update:
* flightwaypointtype.py: Add a new waypoint type if necessary. Most mission types
will need these, as aircraft.py uses the ingress point type to specialize AI
will need these, as aircraftgenerator.py uses the ingress point type to specialize AI
tasks, and non-strike-like missions will need more specialized control.
* ai_flight_planner.py: Use the new mission type in propose_missions so the AI will
plan the new mission type.

View File

@ -1,14 +1,18 @@
from __future__ import annotations
from datetime import timedelta
from typing import Optional, Sequence, Union
from typing import Optional, Sequence, TYPE_CHECKING, Union
from dcs import Point
from dcs.point import MovingPoint, PointAction
from dcs.unit import Unit
from game.theater import ControlPoint, MissionTarget
from game.utils import Distance, meters
from game.ato.flightwaypointtype import FlightWaypointType
if TYPE_CHECKING:
from game.theater import ControlPoint, MissionTarget
class FlightWaypoint:
def __init__(

View File

@ -12,7 +12,7 @@ class FlightWaypointType(Enum):
* waypointbuilder.py: Add a builder to simplify construction of the new waypoint
type unless the new waypoint type will be a parameter to an existing builder
method (such as how escort ingress waypoints work).
* aircraft.py: Associate AI actions with the new waypoint type by subclassing
* aircraftgenerator.py: Associate AI actions with the new waypoint type by subclassing
PydcsWaypointBuilder and using it in PydcsWaypointBuilder.for_waypoint.
"""

View File

@ -1,16 +1,20 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass, field
from datetime import timedelta
from typing import List, Optional, Dict
from typing import List, Optional, Dict, TYPE_CHECKING
from game.ato import Flight, FlightType
from game.ato.packagewaypoints import PackageWaypoints
from game.theater import MissionTarget
from game.utils import Speed
from gen.flights.flightplan import FormationFlightPlan
from gen.flights.traveltime import TotEstimator
if TYPE_CHECKING:
from game.theater import MissionTarget
@dataclass
class Package:

View File

@ -40,9 +40,9 @@ from game.utils import (
)
if TYPE_CHECKING:
from gen.aircraft import FlightData
from gen.airsupport import AirSupport
from gen.radios import Radio, RadioFrequency, RadioRegistry
from game.missiongenerator.aircraftgenerator import FlightData
from game.missiongenerator.airsupport import AirSupport
from game.radio.radios import Radio, RadioFrequency, RadioRegistry
@dataclass(frozen=True)
@ -63,7 +63,7 @@ class RadioConfig:
@classmethod
def make_radio(cls, name: Optional[str]) -> Optional[Radio]:
from gen.radios import get_radio
from game.radio.radios import get_radio
if name is None:
return None
@ -255,7 +255,7 @@ class AircraftType(UnitType[Type[FlyingType]]):
return min(Speed.from_mach(0.35, altitude), max_speed * 0.7)
def alloc_flight_radio(self, radio_registry: RadioRegistry) -> RadioFrequency:
from gen.radios import ChannelInUseError, kHz
from game.radio.radios import ChannelInUseError, kHz
if self.intra_flight_radio is not None:
return radio_registry.alloc_for_radio(self.intra_flight_radio)

View File

@ -8,10 +8,10 @@ from dcs.task import Task
from game import persistency
from game.debriefing import Debriefing
from game.operation.operation import Operation
from game.theater import ControlPoint
from ..ato.airtaaskingorder import AirTaskingOrder
from gen.ground_forces.combat_stance import CombatStance
from ..ato.airtaaskingorder import AirTaskingOrder
from ..missiongenerator import MissionGenerator
from ..unitmap import UnitMap
if TYPE_CHECKING:
@ -58,12 +58,9 @@ class Event:
return []
def generate(self) -> UnitMap:
Operation.prepare(self.game)
unit_map = Operation.generate()
Operation.current_mission.save(
return MissionGenerator(self.game).generate_miz(
persistency.mission_path_for("liberation_nextturn.miz")
)
return unit_map
def commit_air_losses(self, debriefing: Debriefing) -> None:
for loss in debriefing.air_losses.losses:

View File

@ -37,7 +37,9 @@ from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
from .weather import Conditions, TimeOfDay
if TYPE_CHECKING:
from gen.conflictgen import Conflict
from game.missiongenerator.frontlineconflictdescription import (
FrontLineConflictDescription,
)
from .ato.airtaaskingorder import AirTaskingOrder
from .navmesh import NavMesh
from .squadrons import AirWing
@ -453,13 +455,17 @@ class Game:
Compute the current conflict center position(s), mainly used for culling calculation
:return: List of points of interests
"""
from gen.conflictgen import Conflict
from game.missiongenerator.frontlineconflictdescription import (
FrontLineConflictDescription,
)
zones = []
# By default, use the existing frontline conflict position
for front_line in self.theater.conflicts():
position = Conflict.frontline_position(front_line, self.theater)
position = FrontLineConflictDescription.frontline_position(
front_line, self.theater
)
zones.append(position[0])
zones.append(front_line.blue_cp.position)
zones.append(front_line.red_cp.position)

View File

@ -0,0 +1 @@
from .missiongenerator import MissionGenerator

View File

@ -91,13 +91,11 @@ from game.ato.flighttype import FlightType
from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.flightwaypoint import FlightWaypoint
from game.ato.flight import Flight
from gen.lasercoderegistry import LaserCodeRegistry
from gen.radios import RadioFrequency, RadioRegistry
from game.radio.radios import RadioFrequency, RadioRegistry
from gen.runways import RunwayData
from gen.tacan import TacanBand, TacanRegistry, TacanUsage
from .airsupport import AirSupport, AwacsInfo, TankerInfo
from .callsigns import callsign_for_support_unit
from .flights.flightplan import (
from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage
from gen.callsigns import callsign_for_support_unit
from gen.flights.flightplan import (
AwacsFlightPlan,
CasFlightPlan,
LoiterFlightPlan,
@ -105,8 +103,11 @@ from .flights.flightplan import (
RefuelingFlightPlan,
SweepFlightPlan,
)
from .flights.traveltime import GroundSpeed, TotEstimator
from .naming import namegen
from gen.flights.traveltime import GroundSpeed, TotEstimator
from gen.naming import namegen
from .airsupport import AirSupport, AwacsInfo, TankerInfo
from .lasercoderegistry import LaserCodeRegistry
if TYPE_CHECKING:
from game import Game
@ -219,7 +220,7 @@ class FlightData:
)
class AircraftConflictGenerator:
class AircraftGenerator:
def __init__(
self,
mission: Mission,
@ -282,7 +283,7 @@ class AircraftConflictGenerator:
):
return DcsStartType.Runway
else:
return AircraftConflictGenerator._start_type(start_type)
return AircraftGenerator._start_type(start_type)
def skill_level_for(
self, unit: FlyingUnit, pilot: Optional[Pilot], blue: bool

View File

@ -5,8 +5,8 @@ from datetime import timedelta
from typing import Optional, TYPE_CHECKING
if TYPE_CHECKING:
from gen.radios import RadioFrequency
from gen.tacan import TacanChannel
from game.radio.radios import RadioFrequency
from game.radio.tacan import TacanChannel
@dataclass

View File

@ -16,13 +16,14 @@ from dcs.task import (
from dcs.unittype import UnitType
from game.utils import Heading
from gen.callsigns import callsign_for_support_unit
from gen.flights.ai_flight_planner_db import AEWC_CAPABLE
from gen.naming import namegen
from game.radio.radios import RadioRegistry
from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage
from .airsupport import AirSupport, TankerInfo, AwacsInfo
from .callsigns import callsign_for_support_unit
from .conflictgen import Conflict
from .flights.ai_flight_planner_db import AEWC_CAPABLE
from .naming import namegen
from .radios import RadioRegistry
from .tacan import TacanBand, TacanRegistry, TacanUsage
from .frontlineconflictdescription import FrontLineConflictDescription
if TYPE_CHECKING:
from game import Game
@ -35,11 +36,11 @@ AWACS_DISTANCE = 150000
AWACS_ALT = 13000
class AirSupportConflictGenerator:
class AirSupportGenerator:
def __init__(
self,
mission: Mission,
conflict: Conflict,
conflict: FrontLineConflictDescription,
game: Game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,

View File

@ -4,8 +4,8 @@ import json
from pathlib import Path
from typing import Iterable, Optional
from gen.radios import RadioFrequency
from gen.tacan import TacanBand, TacanChannel
from game.radio.radios import RadioFrequency
from game.radio.tacan import TacanBand, TacanChannel
BEACONS_RESOURCE_PATH = Path("resources/dcs/beacons")

View File

@ -12,13 +12,14 @@ from dcs.mission import Mission
from jinja2 import Environment, FileSystemLoader, select_autoescape
from game.theater import ControlPoint, FrontLine
from .aircraft import FlightData
from .airsupportgen import AwacsInfo, TankerInfo
from .armor import JtacInfo
from game.ato.flightwaypoint import FlightWaypoint
from .ground_forces.combat_stance import CombatStance
from .radios import RadioFrequency
from .runways import RunwayData
from gen.ground_forces.combat_stance import CombatStance
from game.radio.radios import RadioFrequency
from gen.runways import RunwayData
from .aircraftgenerator import FlightData
from .airsupportgenerator import AwacsInfo, TankerInfo
from .flotgenerator import JtacInfo
if TYPE_CHECKING:

View File

@ -3,7 +3,6 @@ from __future__ import annotations
import logging
import math
import random
from dataclasses import dataclass
from typing import TYPE_CHECKING, List, Optional, Tuple
from dcs import Mission
@ -40,13 +39,14 @@ from gen.ground_forces.ai_ground_planner import (
CombatGroup,
CombatGroupRole,
)
from gen.callsigns import callsign_for_support_unit
from gen.ground_forces.combat_stance import CombatStance
from gen.naming import namegen
from game.radio.radios import RadioRegistry
from .airsupport import AirSupport, JtacInfo
from .callsigns import callsign_for_support_unit
from .conflictgen import Conflict
from .ground_forces.combat_stance import CombatStance
from .frontlineconflictdescription import FrontLineConflictDescription
from .lasercoderegistry import LaserCodeRegistry
from .naming import namegen
from .radios import MHz, RadioFrequency, RadioRegistry
if TYPE_CHECKING:
from game import Game
@ -69,11 +69,11 @@ RANDOM_OFFSET_ATTACK = 250
INFANTRY_GROUP_SIZE = 5
class GroundConflictGenerator:
class FlotGenerator:
def __init__(
self,
mission: Mission,
conflict: Conflict,
conflict: FrontLineConflictDescription,
game: Game,
player_planned_combat_groups: List[CombatGroup],
enemy_planned_combat_groups: List[CombatGroup],
@ -97,11 +97,11 @@ class GroundConflictGenerator:
self.laser_code_registry = laser_code_registry
def generate(self) -> None:
position = Conflict.frontline_position(
position = FrontLineConflictDescription.frontline_position(
self.conflict.front_line, self.game.theater
)
frontline_vector = Conflict.frontline_vector(
frontline_vector = FrontLineConflictDescription.frontline_vector(
self.conflict.front_line, self.game.theater
)
@ -712,7 +712,7 @@ class GroundConflictGenerator:
desired_point = shifted.point_from_heading(
spawn_heading.degrees, distance_from_frontline
)
return Conflict.find_ground_position(
return FrontLineConflictDescription.find_ground_position(
desired_point, combat_width, heading, self.conflict.theater
)

View File

@ -15,7 +15,7 @@ from game.utils import Heading
FRONTLINE_LENGTH = 80000
class Conflict:
class FrontLineConflictDescription:
def __init__(
self,
theater: ConflictTheater,
@ -95,7 +95,7 @@ class Conflict:
defender: Country,
front_line: FrontLine,
theater: ConflictTheater,
) -> Conflict:
) -> FrontLineConflictDescription:
assert cls.has_frontline_between(front_line.blue_cp, front_line.red_cp)
position, heading, distance = cls.frontline_vector(front_line, theater)
conflict = cls(

View File

@ -42,14 +42,15 @@ from game.theater import ConflictTheater, TheaterGroundObject, LatLon
from game.theater.bullseye import Bullseye
from game.utils import meters
from game.weather import Weather
from .aircraft import FlightData
from .airsupportgen import AwacsInfo, TankerInfo
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
from game.ato.flighttype import FlightType
from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.flightwaypoint import FlightWaypoint
from .radios import RadioFrequency
from .runways import RunwayData
from game.radio.radios import RadioFrequency
from gen.runways import RunwayData
from .aircraftgenerator import FlightData
from .airsupportgenerator import AwacsInfo, TankerInfo
from .briefinggenerator import CommInfo, JtacInfo, MissionInfoGenerator
if TYPE_CHECKING:
from game import Game

View File

@ -0,0 +1,309 @@
from __future__ import annotations
import logging
import os
from pathlib import Path
from typing import TYPE_CHECKING
from dcs import Mission
from dcs.action import DoScript, DoScriptFile
from dcs.translation import String
from dcs.triggers import TriggerStart
from game.ato import FlightType
from game.plugins import LuaPluginManager
from game.theater import TheaterGroundObject
from .aircraftgenerator import FlightData
from .airsupport import AirSupport
if TYPE_CHECKING:
from game import Game
class LuaGenerator:
def __init__(
self,
game: Game,
mission: Mission,
air_support: AirSupport,
flights: list[FlightData],
) -> None:
self.game = game
self.mission = mission
self.air_support = air_support
self.flights = flights
self.plugin_scripts: list[str] = []
def generate(self) -> None:
self.generate_plugin_data()
self.inject_plugins()
def generate_plugin_data(self) -> None:
# TODO: Refactor this
lua_data = {
"AircraftCarriers": {},
"Tankers": {},
"AWACs": {},
"JTACs": {},
"TargetPoints": {},
"RedAA": {},
"BlueAA": {},
} # type: ignore
for i, tanker in enumerate(self.air_support.tankers):
lua_data["Tankers"][i] = {
"dcsGroupName": tanker.group_name,
"callsign": tanker.callsign,
"variant": tanker.variant,
"radio": tanker.freq.mhz,
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name,
}
for i, awacs in enumerate(self.air_support.awacs):
lua_data["AWACs"][i] = {
"dcsGroupName": awacs.group_name,
"callsign": awacs.callsign,
"radio": awacs.freq.mhz,
}
for i, jtac in enumerate(self.air_support.jtacs):
lua_data["JTACs"][i] = {
"dcsGroupName": jtac.group_name,
"callsign": jtac.callsign,
"zone": jtac.region,
"dcsUnit": jtac.unit_name,
"laserCode": jtac.code,
"radio": jtac.freq.mhz,
}
flight_count = 0
for flight in self.flights:
if flight.friendly and flight.flight_type in [
FlightType.ANTISHIP,
FlightType.DEAD,
FlightType.SEAD,
FlightType.STRIKE,
]:
flight_type = str(flight.flight_type)
flight_target = flight.package.target
if flight_target:
flight_target_name = None
flight_target_type = None
if isinstance(flight_target, TheaterGroundObject):
flight_target_name = flight_target.obj_name
flight_target_type = (
flight_type + f" TGT ({flight_target.category})"
)
elif hasattr(flight_target, "name"):
flight_target_name = flight_target.name
flight_target_type = flight_type + " TGT (Airbase)"
lua_data["TargetPoints"][flight_count] = {
"name": flight_target_name,
"type": flight_target_type,
"position": {
"x": flight_target.position.x,
"y": flight_target.position.y,
},
}
flight_count += 1
for cp in self.game.theater.controlpoints:
for ground_object in cp.ground_objects:
if ground_object.might_have_aa and not ground_object.is_dead:
for g in ground_object.groups:
threat_range = ground_object.threat_range(g)
if not threat_range:
continue
faction = "BlueAA" if cp.captured else "RedAA"
lua_data[faction][g.name] = {
"name": ground_object.name,
"range": threat_range.meters,
"position": {
"x": ground_object.position.x,
"y": ground_object.position.y,
},
}
# set a LUA table with data from Liberation that we want to set
# at the moment it contains Liberation's install path, and an overridable
# definition for the JTACAutoLase function later, we'll add data about the units
# and points having been generated, in order to facilitate the configuration of
# the plugin lua scripts
state_location = "[[" + os.path.abspath(".") + "]]"
lua = (
"""
-- setting configuration table
env.info("DCSLiberation|: setting configuration table")
-- all data in this table is overridable.
dcsLiberation = {}
-- the base location for state.json; if non-existent, it'll be replaced with
-- LIBERATION_EXPORT_DIR, TEMP, or DCS working directory
dcsLiberation.installPath="""
+ state_location
+ """
"""
)
# Process the tankers
lua += """
-- list the tankers generated by Liberation
dcsLiberation.Tankers = {
"""
for key in lua_data["Tankers"]:
data = lua_data["Tankers"][key]
dcs_group_name = data["dcsGroupName"]
callsign = data["callsign"]
variant = data["variant"]
tacan = data["tacan"]
radio = data["radio"]
lua += (
f" {{dcsGroupName='{dcs_group_name}', callsign='{callsign}', "
f"variant='{variant}', tacan='{tacan}', radio='{radio}' }}, \n"
)
lua += "}"
# Process the AWACSes
lua += """
-- list the AWACs generated by Liberation
dcsLiberation.AWACs = {
"""
for key in lua_data["AWACs"]:
data = lua_data["AWACs"][key]
dcs_group_name = data["dcsGroupName"]
callsign = data["callsign"]
radio = data["radio"]
lua += (
f" {{dcsGroupName='{dcs_group_name}', callsign='{callsign}', "
f"radio='{radio}' }}, \n"
)
lua += "}"
# Process the JTACs
lua += """
-- list the JTACs generated by Liberation
dcsLiberation.JTACs = {
"""
for key in lua_data["JTACs"]:
data = lua_data["JTACs"][key]
dcs_group_name = data["dcsGroupName"]
callsign = data["callsign"]
zone = data["zone"]
laser_code = data["laserCode"]
dcs_unit = data["dcsUnit"]
radio = data["radio"]
lua += (
f" {{dcsGroupName='{dcs_group_name}', callsign='{callsign}', "
f"zone={repr(zone)}, laserCode='{laser_code}', dcsUnit='{dcs_unit}', "
f"radio='{radio}' }}, \n"
)
lua += "}"
# Process the Target Points
lua += """
-- list the target points generated by Liberation
dcsLiberation.TargetPoints = {
"""
for key in lua_data["TargetPoints"]:
data = lua_data["TargetPoints"][key]
name = data["name"]
point_type = data["type"]
position_x = data["position"]["x"]
position_y = data["position"]["y"]
lua += (
f" {{name='{name}', pointType='{point_type}', "
f"positionX='{position_x}', positionY='{position_y}' }}, \n"
)
lua += "}"
lua += """
-- list the airbases generated by Liberation
-- dcsLiberation.Airbases = {}
-- list the aircraft carriers generated by Liberation
-- dcsLiberation.Carriers = {}
-- list the Red AA generated by Liberation
dcsLiberation.RedAA = {
"""
for key in lua_data["RedAA"]:
data = lua_data["RedAA"][key]
name = data["name"]
radius = data["range"]
position_x = data["position"]["x"]
position_y = data["position"]["y"]
lua += (
f" {{dcsGroupName='{key}', name='{name}', range='{radius}', "
f"positionX='{position_x}', positionY='{position_y}' }}, \n"
)
lua += "}"
lua += """
-- list the Blue AA generated by Liberation
dcsLiberation.BlueAA = {
"""
for key in lua_data["BlueAA"]:
data = lua_data["BlueAA"][key]
name = data["name"]
radius = data["range"]
position_x = data["position"]["x"]
position_y = data["position"]["y"]
lua += (
f" {{dcsGroupName='{key}', name='{name}', range='{radius}', "
f"positionX='{position_x}', positionY='{position_y}' }}, \n"
)
lua += "}"
lua += """
"""
trigger = TriggerStart(comment="Set DCS Liberation data")
trigger.add_action(DoScript(String(lua)))
self.mission.triggerrules.triggers.append(trigger)
def inject_lua_trigger(self, contents: str, comment: str) -> None:
trigger = TriggerStart(comment=comment)
trigger.add_action(DoScript(String(contents)))
self.mission.triggerrules.triggers.append(trigger)
def bypass_plugin_script(self, mnemonic: str) -> None:
self.plugin_scripts.append(mnemonic)
def inject_plugin_script(
self, plugin_mnemonic: str, script: str, script_mnemonic: str
) -> None:
if script_mnemonic in self.plugin_scripts:
logging.debug(f"Skipping already loaded {script} for {plugin_mnemonic}")
return
self.plugin_scripts.append(script_mnemonic)
plugin_path = Path("./resources/plugins", plugin_mnemonic)
script_path = Path(plugin_path, script)
if not script_path.exists():
logging.error(f"Cannot find {script_path} for plugin {plugin_mnemonic}")
return
trigger = TriggerStart(comment=f"Load {script_mnemonic}")
filename = script_path.resolve()
fileref = self.mission.map_resource.add_resource_file(filename)
trigger.add_action(DoScriptFile(fileref))
self.mission.triggerrules.triggers.append(trigger)
def inject_plugins(self) -> None:
for plugin in LuaPluginManager.plugins():
if plugin.enabled:
plugin.inject_scripts(self)
plugin.inject_configuration(self)

View File

@ -0,0 +1,346 @@
from __future__ import annotations
import logging
from pathlib import Path
from typing import TYPE_CHECKING, cast
import dcs.lua
from dcs import Mission, Point
from dcs.coalition import Coalition
from dcs.countries import country_dict
from game import db
from game.radio.radios import RadioFrequency, RadioRegistry
from game.radio.tacan import TacanRegistry
from game.theater.bullseye import Bullseye
from game.theater import Airfield, FrontLine
from game.unitmap import UnitMap
from gen.airfields import AIRFIELD_DATA
from gen.naming import namegen
from .aircraftgenerator import AircraftGenerator, FlightData
from .airsupport import AirSupport
from .airsupportgenerator import AirSupportGenerator
from .beacons import load_beacons_for_terrain
from .briefinggenerator import BriefingGenerator, MissionInfoGenerator
from .cargoshipgenerator import CargoShipGenerator
from .convoygenerator import ConvoyGenerator
from .environmentgenerator import EnvironmentGenerator
from .flotgenerator import FlotGenerator
from .forcedoptionsgenerator import ForcedOptionsGenerator
from .frontlineconflictdescription import FrontLineConflictDescription
from .kneeboard import KneeboardGenerator
from .lasercoderegistry import LaserCodeRegistry
from .luagenerator import LuaGenerator
from .tgogenerator import TgoGenerator
from .triggergenerator import TriggerGenerator
from .visualsgenerator import VisualsGenerator
if TYPE_CHECKING:
from game import Game
COMBINED_ARMS_SLOTS = 1
class MissionGenerator:
def __init__(self, game: Game) -> None:
self.game = game
self.mission = Mission(game.theater.terrain)
self.unit_map = UnitMap()
self.air_support = AirSupport()
self.laser_code_registry = LaserCodeRegistry()
self.radio_registry = RadioRegistry()
self.tacan_registry = TacanRegistry()
self.generation_started = False
with open("resources/default_options.lua", "r", encoding="utf-8") as f:
self.mission.options.load_from_dict(dcs.lua.loads(f.read())["options"])
def generate_miz(self, output: Path) -> UnitMap:
if self.generation_started:
raise RuntimeError(
"Mission has already begun generating. To reset, create a new "
"MissionSimulation."
)
self.generation_started = True
self.setup_mission_coalitions()
self.add_airfields_to_unit_map()
self.initialize_registries()
EnvironmentGenerator(self.mission, self.game.conditions).generate()
tgo_generator = TgoGenerator(
self.mission,
self.game,
self.radio_registry,
self.tacan_registry,
self.unit_map,
)
tgo_generator.generate()
ConvoyGenerator(self.mission, self.game, self.unit_map).generate()
CargoShipGenerator(self.mission, self.game, self.unit_map).generate()
self.generate_destroyed_units()
# 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()
air_support, flights = self.generate_air_units(tgo_generator)
TriggerGenerator(self.mission, self.game).generate()
ForcedOptionsGenerator(self.mission, self.game).generate()
VisualsGenerator(self.mission, self.game).generate()
LuaGenerator(self.game, self.mission, air_support, flights).generate()
self.setup_combined_arms()
self.notify_info_generators(tgo_generator, air_support, flights)
# TODO: Shouldn't this be first?
namegen.reset_numbers()
self.mission.save(output)
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)).to_pydcs()
)
p_country = self.game.blue.country_name
e_country = self.game.red.country_name
self.mission.coalition["blue"].add_country(
country_dict[db.country_id_from_name(p_country)]()
)
self.mission.coalition["red"].add_country(
country_dict[db.country_id_from_name(e_country)]()
)
belligerents = [
db.country_id_from_name(p_country),
db.country_id_from_name(e_country),
]
for country in country_dict.keys():
if country not in belligerents:
self.mission.coalition["neutrals"].add_country(country_dict[country]())
def add_airfields_to_unit_map(self) -> None:
for control_point in self.game.theater.controlpoints:
if isinstance(control_point, Airfield):
self.unit_map.add_airfield(control_point)
def initialize_registries(self) -> None:
unique_map_frequencies: set[RadioFrequency] = set()
self.initialize_tacan_registry(unique_map_frequencies)
self.initialize_radio_registry(unique_map_frequencies)
for frequency in unique_map_frequencies:
self.radio_registry.reserve(frequency)
def initialize_tacan_registry(
self, unique_map_frequencies: set[RadioFrequency]
) -> None:
"""
Dedup beacon/radio frequencies, since some maps have some frequencies
used multiple times.
"""
beacons = load_beacons_for_terrain(self.game.theater.terrain.name)
for beacon in beacons:
unique_map_frequencies.add(beacon.frequency)
if beacon.is_tacan:
if beacon.channel is None:
logging.warning(f"TACAN beacon has no channel: {beacon.callsign}")
else:
self.tacan_registry.mark_unavailable(beacon.tacan_channel)
def initialize_radio_registry(
self, unique_map_frequencies: set[RadioFrequency]
) -> None:
for data in AIRFIELD_DATA.values():
if data.theater == self.game.theater.terrain.name and data.atc:
unique_map_frequencies.add(data.atc.hf)
unique_map_frequencies.add(data.atc.vhf_fm)
unique_map_frequencies.add(data.atc.vhf_am)
unique_map_frequencies.add(data.atc.uhf)
# No need to reserve ILS or TACAN because those are in the
# beacon list.
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
conflict = FrontLineConflictDescription.frontline_cas_conflict(
self.game.blue.faction.name,
self.game.red.faction.name,
self.mission.country(self.game.blue.country_name),
self.mission.country(self.game.red.country_name),
front_line,
self.game.theater,
)
# Generate frontline ops
player_gp = self.game.ground_planners[player_cp.id].units_per_cp[
enemy_cp.id
]
enemy_gp = self.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id]
ground_conflict_gen = FlotGenerator(
self.mission,
conflict,
self.game,
player_gp,
enemy_gp,
player_cp.stances[enemy_cp.id],
enemy_cp.stances[player_cp.id],
self.unit_map,
self.radio_registry,
self.air_support,
self.laser_code_registry,
)
ground_conflict_gen.generate()
def generate_air_units(
self, tgo_generator: TgoGenerator
) -> tuple[AirSupport, list[FlightData]]:
"""Generate the air units for the Operation"""
# Air Support (Tanker & Awacs)
air_support_generator = AirSupportGenerator(
self.mission,
self.describe_air_conflict(),
self.game,
self.radio_registry,
self.tacan_registry,
self.air_support,
)
air_support_generator.generate()
# Generate Aircraft Activity on the map
aircraft_generator = AircraftGenerator(
self.mission,
self.game.settings,
self.game,
self.radio_registry,
self.tacan_registry,
self.laser_code_registry,
self.unit_map,
air_support=air_support_generator.air_support,
helipads=tgo_generator.helipads,
)
aircraft_generator.clear_parking_slots()
aircraft_generator.generate_flights(
self.mission.country(self.game.blue.country_name),
self.game.blue.ato,
tgo_generator.runways,
)
aircraft_generator.generate_flights(
self.mission.country(self.game.red.country_name),
self.game.red.ato,
tgo_generator.runways,
)
aircraft_generator.spawn_unused_aircraft(
self.mission.country(self.game.blue.country_name),
self.mission.country(self.game.red.country_name),
)
for flight in aircraft_generator.flights:
if not flight.client_units:
continue
flight.aircraft_type.assign_channels_for_flight(
flight, air_support_generator.air_support
)
return air_support_generator.air_support, aircraft_generator.flights
def generate_destroyed_units(self) -> None:
"""Add destroyed units to the Mission"""
if not self.game.settings.perf_destroyed_units:
return
for d in self.game.get_destroyed_units():
try:
type_name = d["type"]
if not isinstance(type_name, str):
raise TypeError(
"Expected the type of the destroyed static to be a string"
)
utype = db.unit_type_from_name(type_name)
except KeyError:
logging.warning(f"Destroyed unit has no type: {d}")
continue
pos = Point(cast(float, d["x"]), cast(float, d["z"]))
if utype is not None and not self.game.position_culled(pos):
self.mission.static_group(
country=self.mission.country(self.game.blue.country_name),
name="",
_type=utype,
hidden=True,
position=pos,
heading=d["orientation"],
dead=True,
)
def describe_air_conflict(self) -> FrontLineConflictDescription:
player_cp, enemy_cp = self.game.theater.closest_opposing_control_points()
mid_point = player_cp.position.point_from_heading(
player_cp.position.heading_between_point(enemy_cp.position),
player_cp.position.distance_to_point(enemy_cp.position) / 2,
)
return FrontLineConflictDescription(
self.game.theater,
FrontLine(player_cp, enemy_cp),
self.game.blue.faction.name,
self.game.red.faction.name,
self.mission.country(self.game.blue.country_name),
self.mission.country(self.game.red.country_name),
mid_point,
)
def notify_info_generators(
self,
tgo_generator: TgoGenerator,
air_support: AirSupport,
flights: list[FlightData],
) -> None:
"""Generates subscribed MissionInfoGenerator objects."""
gens: list[MissionInfoGenerator] = [
KneeboardGenerator(self.mission, self.game),
BriefingGenerator(self.mission, self.game),
]
for gen in gens:
for dynamic_runway in tgo_generator.runways.values():
gen.add_dynamic_runway(dynamic_runway)
for tanker in air_support.tankers:
if tanker.blue:
gen.add_tanker(tanker)
for aewc in air_support.awacs:
if aewc.blue:
gen.add_awacs(aewc)
for jtac in air_support.jtacs:
if jtac.blue:
gen.add_jtac(jtac)
for flight in flights:
gen.add_flight(flight)
gen.generate()
def setup_combined_arms(self) -> None:
self.mission.groundControl.pilot_can_control_vehicles = COMBINED_ARMS_SLOTS > 0
self.mission.groundControl.blue_tactical_commander = COMBINED_ARMS_SLOTS
self.mission.groundControl.blue_observer = 1

View File

@ -57,9 +57,9 @@ from game.theater.theatergroundobject import (
)
from game.unitmap import UnitMap
from game.utils import Heading, feet, knots, mps
from .radios import RadioFrequency, RadioRegistry
from .runways import RunwayData
from .tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
from game.radio.radios import RadioFrequency, RadioRegistry
from gen.runways import RunwayData
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
if TYPE_CHECKING:
from game import Game
@ -183,7 +183,6 @@ class MissileSiteGenerator(GenericGroundObjectGenerator[MissileSiteGroundObject]
def possible_missile_targets(self) -> List[Point]:
"""
Find enemy control points in range
:param vg: Vehicle group we are searching a target for (There is always only oe group right now)
:return: List of possible missile targets
"""
targets: List[Point] = []
@ -312,8 +311,8 @@ class SceneryGenerator(BuildingSiteGenerator):
else:
color = {1: 1, 2: 0.2, 3: 0.2, 4: 0.15}
# Create the smallest valid size trigger zone (16 feet) so that risk of overlap is minimized.
# As long as the triggerzone is over the scenery object, we're ok.
# Create the smallest valid size trigger zone (16 feet) so that risk of overlap
# is minimized. As long as the triggerzone is over the scenery object, we're ok.
smallest_valid_radius = feet(16).meters
return self.m.triggers.add_triggerzone(
@ -594,7 +593,8 @@ class HelipadGenerator:
def generate(self) -> None:
# Note : Helipad are generated as neutral object in order not to interfer with capture triggers
# Note: Helipad are generated as neutral object in order not to interfer with
# capture triggers
neutral_country = self.m.country(self.game.neutral_country.name)
country = self.m.country(self.game.coalition_for(self.cp.captured).country_name)
for i, helipad in enumerate(self.cp.helipads):
@ -631,7 +631,7 @@ class HelipadGenerator:
)
class GroundObjectsGenerator:
class 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

View File

@ -47,7 +47,7 @@ class Silence(Option):
Key = 7
class TriggersGenerator:
class TriggerGenerator:
capture_zone_types = (Fob,)
capture_zone_flag = 600

View File

@ -10,7 +10,7 @@ from dcs.unittype import StaticType
if TYPE_CHECKING:
from game import Game
from .conflictgen import Conflict
from .frontlineconflictdescription import FrontLineConflictDescription
class MarkerSmoke(StaticType):
@ -67,7 +67,7 @@ FRONT_SMOKE_TYPE_CHANCES = {
}
class VisualGenerator:
class VisualsGenerator:
def __init__(self, mission: Mission, game: Game) -> None:
self.mission = mission
self.game = game
@ -79,7 +79,11 @@ class VisualGenerator:
if from_cp.is_global or to_cp.is_global:
continue
plane_start, heading, distance = Conflict.frontline_vector(
(
plane_start,
heading,
distance,
) = FrontLineConflictDescription.frontline_vector(
front_line, self.game.theater
)
if not plane_start:
@ -105,4 +109,5 @@ class VisualGenerator:
break
def generate(self) -> None:
self._generate_frontline_smokes()
if self.game.settings.perf_smoke_gen:
self._generate_frontline_smokes()

View File

@ -1,659 +0,0 @@
from __future__ import annotations
import logging
import os
from pathlib import Path
from typing import List, Set, TYPE_CHECKING, cast
from dcs import Mission
from dcs.action import DoScript, DoScriptFile
from dcs.coalition import Coalition
from dcs.countries import country_dict
from dcs.lua.parse import loads
from dcs.mapping import Point
from dcs.translation import String
from dcs.triggers import TriggerStart
from game.plugins import LuaPluginManager
from game.theater.theatergroundobject import TheaterGroundObject
from gen.aircraft import AircraftConflictGenerator, FlightData
from gen.airfields import AIRFIELD_DATA
from gen.airsupport import AirSupport
from gen.airsupportgen import AirSupportConflictGenerator
from gen.armor import GroundConflictGenerator
from gen.beacons import load_beacons_for_terrain
from gen.briefinggen import BriefingGenerator, MissionInfoGenerator
from gen.cargoshipgen import CargoShipGenerator
from gen.conflictgen import Conflict
from gen.convoygen import ConvoyGenerator
from gen.environmentgen import EnvironmentGenerator
from ..ato.flighttype import FlightType
from gen.forcedoptionsgen import ForcedOptionsGenerator
from gen.groundobjectsgen import GroundObjectsGenerator
from gen.kneeboard import KneeboardGenerator
from gen.lasercoderegistry import LaserCodeRegistry
from gen.naming import namegen
from gen.radios import RadioFrequency, RadioRegistry
from gen.tacan import TacanRegistry, TacanUsage
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
from gen.visualgen import VisualGenerator
from .. import db
from ..theater import Airfield, FrontLine
from ..theater.bullseye import Bullseye
from ..unitmap import UnitMap
if TYPE_CHECKING:
from game import Game
class Operation:
"""Static class for managing the final Mission generation"""
current_mission: Mission
airgen: AircraftConflictGenerator
airsupportgen: AirSupportConflictGenerator
groundobjectgen: GroundObjectsGenerator
radio_registry: RadioRegistry
tacan_registry: TacanRegistry
laser_code_registry: LaserCodeRegistry
game: Game
trigger_radius = TRIGGER_RADIUS_MEDIUM
is_quick = None
player_awacs_enabled = True
# TODO: #436 Generate Air Support for red
enemy_awacs_enabled = True
ca_slots = 1
unit_map: UnitMap
plugin_scripts: List[str] = []
air_support = AirSupport()
@classmethod
def prepare(cls, game: Game) -> None:
with open("resources/default_options.lua", "r", encoding="utf-8") as f:
options_dict = loads(f.read())["options"]
cls._set_mission(Mission(game.theater.terrain))
cls.game = game
cls._setup_mission_coalitions()
cls.current_mission.options.load_from_dict(options_dict)
@classmethod
def air_conflict(cls) -> Conflict:
assert cls.game
player_cp, enemy_cp = cls.game.theater.closest_opposing_control_points()
mid_point = player_cp.position.point_from_heading(
player_cp.position.heading_between_point(enemy_cp.position),
player_cp.position.distance_to_point(enemy_cp.position) / 2,
)
return Conflict(
cls.game.theater,
FrontLine(player_cp, enemy_cp),
cls.game.blue.faction.name,
cls.game.red.faction.name,
cls.current_mission.country(cls.game.blue.country_name),
cls.current_mission.country(cls.game.red.country_name),
mid_point,
)
@classmethod
def _set_mission(cls, mission: Mission) -> None:
cls.current_mission = mission
@classmethod
def _setup_mission_coalitions(cls) -> None:
cls.current_mission.coalition["blue"] = Coalition(
"blue", bullseye=cls.game.blue.bullseye.to_pydcs()
)
cls.current_mission.coalition["red"] = Coalition(
"red", bullseye=cls.game.red.bullseye.to_pydcs()
)
cls.current_mission.coalition["neutrals"] = Coalition(
"neutrals", bullseye=Bullseye(Point(0, 0)).to_pydcs()
)
p_country = cls.game.blue.country_name
e_country = cls.game.red.country_name
cls.current_mission.coalition["blue"].add_country(
country_dict[db.country_id_from_name(p_country)]()
)
cls.current_mission.coalition["red"].add_country(
country_dict[db.country_id_from_name(e_country)]()
)
belligerents = [
db.country_id_from_name(p_country),
db.country_id_from_name(e_country),
]
for country in country_dict.keys():
if country not in belligerents:
cls.current_mission.coalition["neutrals"].add_country(
country_dict[country]()
)
@classmethod
def inject_lua_trigger(cls, contents: str, comment: str) -> None:
trigger = TriggerStart(comment=comment)
trigger.add_action(DoScript(String(contents)))
cls.current_mission.triggerrules.triggers.append(trigger)
@classmethod
def bypass_plugin_script(cls, mnemonic: str) -> None:
cls.plugin_scripts.append(mnemonic)
@classmethod
def inject_plugin_script(
cls, plugin_mnemonic: str, script: str, script_mnemonic: str
) -> None:
if script_mnemonic in cls.plugin_scripts:
logging.debug(f"Skipping already loaded {script} for {plugin_mnemonic}")
else:
cls.plugin_scripts.append(script_mnemonic)
plugin_path = Path("./resources/plugins", plugin_mnemonic)
script_path = Path(plugin_path, script)
if not script_path.exists():
logging.error(f"Cannot find {script_path} for plugin {plugin_mnemonic}")
return
trigger = TriggerStart(comment=f"Load {script_mnemonic}")
filename = script_path.resolve()
fileref = cls.current_mission.map_resource.add_resource_file(filename)
trigger.add_action(DoScriptFile(fileref))
cls.current_mission.triggerrules.triggers.append(trigger)
@classmethod
def notify_info_generators(
cls,
groundobjectgen: GroundObjectsGenerator,
air_support: AirSupport,
airgen: AircraftConflictGenerator,
) -> None:
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)"""
gens: List[MissionInfoGenerator] = [
KneeboardGenerator(cls.current_mission, cls.game),
BriefingGenerator(cls.current_mission, cls.game),
]
for gen in gens:
for dynamic_runway in groundobjectgen.runways.values():
gen.add_dynamic_runway(dynamic_runway)
for tanker in air_support.tankers:
if tanker.blue:
gen.add_tanker(tanker)
for aewc in air_support.awacs:
if aewc.blue:
gen.add_awacs(aewc)
for jtac in air_support.jtacs:
if jtac.blue:
gen.add_jtac(jtac)
for flight in airgen.flights:
gen.add_flight(flight)
gen.generate()
@classmethod
def create_unit_map(cls) -> None:
cls.unit_map = UnitMap()
for control_point in cls.game.theater.controlpoints:
if isinstance(control_point, Airfield):
cls.unit_map.add_airfield(control_point)
@classmethod
def create_radio_registries(cls) -> None:
unique_map_frequencies: Set[RadioFrequency] = set()
cls._create_tacan_registry(unique_map_frequencies)
cls._create_radio_registry(unique_map_frequencies)
assert cls.radio_registry is not None
for frequency in unique_map_frequencies:
cls.radio_registry.reserve(frequency)
@classmethod
def create_laser_code_registry(cls) -> None:
cls.laser_code_registry = LaserCodeRegistry()
@classmethod
def assign_channels_to_flights(
cls, flights: List[FlightData], air_support: AirSupport
) -> None:
"""Assigns preset radio channels for client flights."""
for flight in flights:
if not flight.client_units:
continue
flight.aircraft_type.assign_channels_for_flight(flight, air_support)
@classmethod
def _create_tacan_registry(
cls, unique_map_frequencies: Set[RadioFrequency]
) -> None:
"""
Dedup beacon/radio frequencies, since some maps have some frequencies
used multiple times.
"""
cls.tacan_registry = TacanRegistry()
beacons = load_beacons_for_terrain(cls.game.theater.terrain.name)
for beacon in beacons:
unique_map_frequencies.add(beacon.frequency)
if beacon.is_tacan:
if beacon.channel is None:
logging.error(f"TACAN beacon has no channel: {beacon.callsign}")
else:
cls.tacan_registry.mark_unavailable(beacon.tacan_channel)
@classmethod
def _create_radio_registry(
cls, unique_map_frequencies: Set[RadioFrequency]
) -> None:
cls.radio_registry = RadioRegistry()
for data in AIRFIELD_DATA.values():
if data.theater == cls.game.theater.terrain.name and data.atc:
unique_map_frequencies.add(data.atc.hf)
unique_map_frequencies.add(data.atc.vhf_fm)
unique_map_frequencies.add(data.atc.vhf_am)
unique_map_frequencies.add(data.atc.uhf)
# No need to reserve ILS or TACAN because those are in the
# beacon list.
@classmethod
def _generate_ground_units(cls) -> None:
cls.groundobjectgen = GroundObjectsGenerator(
cls.current_mission,
cls.game,
cls.radio_registry,
cls.tacan_registry,
cls.unit_map,
)
cls.groundobjectgen.generate()
@classmethod
def _generate_destroyed_units(cls) -> None:
"""Add destroyed units to the Mission"""
for d in cls.game.get_destroyed_units():
try:
type_name = d["type"]
if not isinstance(type_name, str):
raise TypeError(
"Expected the type of the destroyed static to be a string"
)
utype = db.unit_type_from_name(type_name)
except KeyError:
continue
pos = Point(cast(float, d["x"]), cast(float, d["z"]))
if (
utype is not None
and not cls.game.position_culled(pos)
and cls.game.settings.perf_destroyed_units
):
cls.current_mission.static_group(
country=cls.current_mission.country(cls.game.blue.country_name),
name="",
_type=utype,
hidden=True,
position=pos,
heading=d["orientation"],
dead=True,
)
@classmethod
def generate(cls) -> UnitMap:
"""Build the final Mission to be exported"""
cls.air_support = AirSupport()
cls.create_unit_map()
cls.create_radio_registries()
cls.create_laser_code_registry()
# Set mission time and weather conditions.
EnvironmentGenerator(cls.current_mission, cls.game.conditions).generate()
cls._generate_ground_units()
cls._generate_transports()
cls._generate_destroyed_units()
# Generate ground conflicts first so the JTACs get the first laser code (1688)
# rather than the first player flight with a TGP.
cls._generate_ground_conflicts()
cls._generate_air_units()
cls.assign_channels_to_flights(
cls.airgen.flights, cls.airsupportgen.air_support
)
# Triggers
triggersgen = TriggersGenerator(cls.current_mission, cls.game)
triggersgen.generate()
# Setup combined arms parameters
cls.current_mission.groundControl.pilot_can_control_vehicles = cls.ca_slots > 0
cls.current_mission.groundControl.blue_tactical_commander = cls.ca_slots
cls.current_mission.groundControl.blue_observer = 1
# Options
forcedoptionsgen = ForcedOptionsGenerator(cls.current_mission, cls.game)
forcedoptionsgen.generate()
# Generate Visuals Smoke Effects
visualgen = VisualGenerator(cls.current_mission, cls.game)
if cls.game.settings.perf_smoke_gen:
visualgen.generate()
cls.generate_lua(cls.airgen, cls.air_support)
# Inject Plugins Lua Scripts and data
cls.plugin_scripts.clear()
for plugin in LuaPluginManager.plugins():
if plugin.enabled:
plugin.inject_scripts(cls)
plugin.inject_configuration(cls)
cls.assign_channels_to_flights(
cls.airgen.flights, cls.airsupportgen.air_support
)
cls.notify_info_generators(cls.groundobjectgen, cls.air_support, cls.airgen)
cls.reset_naming_ids()
return cls.unit_map
@classmethod
def _generate_air_units(cls) -> None:
"""Generate the air units for the Operation"""
# Air Support (Tanker & Awacs)
assert cls.radio_registry and cls.tacan_registry
cls.airsupportgen = AirSupportConflictGenerator(
cls.current_mission,
cls.air_conflict(),
cls.game,
cls.radio_registry,
cls.tacan_registry,
cls.air_support,
)
cls.airsupportgen.generate()
# Generate Aircraft Activity on the map
cls.airgen = AircraftConflictGenerator(
cls.current_mission,
cls.game.settings,
cls.game,
cls.radio_registry,
cls.tacan_registry,
cls.laser_code_registry,
cls.unit_map,
air_support=cls.airsupportgen.air_support,
helipads=cls.groundobjectgen.helipads,
)
cls.airgen.clear_parking_slots()
cls.airgen.generate_flights(
cls.current_mission.country(cls.game.blue.country_name),
cls.game.blue.ato,
cls.groundobjectgen.runways,
)
cls.airgen.generate_flights(
cls.current_mission.country(cls.game.red.country_name),
cls.game.red.ato,
cls.groundobjectgen.runways,
)
cls.airgen.spawn_unused_aircraft(
cls.current_mission.country(cls.game.blue.country_name),
cls.current_mission.country(cls.game.red.country_name),
)
@classmethod
def _generate_ground_conflicts(cls) -> None:
"""For each frontline in the Operation, generate the ground conflicts and JTACs"""
for front_line in cls.game.theater.conflicts():
player_cp = front_line.blue_cp
enemy_cp = front_line.red_cp
conflict = Conflict.frontline_cas_conflict(
cls.game.blue.faction.name,
cls.game.red.faction.name,
cls.current_mission.country(cls.game.blue.country_name),
cls.current_mission.country(cls.game.red.country_name),
front_line,
cls.game.theater,
)
# Generate frontline ops
player_gp = cls.game.ground_planners[player_cp.id].units_per_cp[enemy_cp.id]
enemy_gp = cls.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id]
ground_conflict_gen = GroundConflictGenerator(
cls.current_mission,
conflict,
cls.game,
player_gp,
enemy_gp,
player_cp.stances[enemy_cp.id],
enemy_cp.stances[player_cp.id],
cls.unit_map,
cls.radio_registry,
cls.air_support,
cls.laser_code_registry,
)
ground_conflict_gen.generate()
@classmethod
def _generate_transports(cls) -> None:
"""Generates convoys for unit transfers by road."""
ConvoyGenerator(cls.current_mission, cls.game, cls.unit_map).generate()
CargoShipGenerator(cls.current_mission, cls.game, cls.unit_map).generate()
@classmethod
def reset_naming_ids(cls) -> None:
namegen.reset_numbers()
@classmethod
def generate_lua(
cls, airgen: AircraftConflictGenerator, air_support: AirSupport
) -> None:
# TODO: Refactor this
luaData = {
"AircraftCarriers": {},
"Tankers": {},
"AWACs": {},
"JTACs": {},
"TargetPoints": {},
"RedAA": {},
"BlueAA": {},
} # type: ignore
for i, tanker in enumerate(air_support.tankers):
luaData["Tankers"][i] = {
"dcsGroupName": tanker.group_name,
"callsign": tanker.callsign,
"variant": tanker.variant,
"radio": tanker.freq.mhz,
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name,
}
for i, awacs in enumerate(air_support.awacs):
luaData["AWACs"][i] = {
"dcsGroupName": awacs.group_name,
"callsign": awacs.callsign,
"radio": awacs.freq.mhz,
}
for i, jtac in enumerate(air_support.jtacs):
luaData["JTACs"][i] = {
"dcsGroupName": jtac.group_name,
"callsign": jtac.callsign,
"zone": jtac.region,
"dcsUnit": jtac.unit_name,
"laserCode": jtac.code,
"radio": jtac.freq.mhz,
}
flight_count = 0
for flight in airgen.flights:
if flight.friendly and flight.flight_type in [
FlightType.ANTISHIP,
FlightType.DEAD,
FlightType.SEAD,
FlightType.STRIKE,
]:
flightType = str(flight.flight_type)
flightTarget = flight.package.target
if flightTarget:
flightTargetName = None
flightTargetType = None
if isinstance(flightTarget, TheaterGroundObject):
flightTargetName = flightTarget.obj_name
flightTargetType = (
flightType + f" TGT ({flightTarget.category})"
)
elif hasattr(flightTarget, "name"):
flightTargetName = flightTarget.name
flightTargetType = flightType + " TGT (Airbase)"
luaData["TargetPoints"][flight_count] = {
"name": flightTargetName,
"type": flightTargetType,
"position": {
"x": flightTarget.position.x,
"y": flightTarget.position.y,
},
}
flight_count += 1
for cp in cls.game.theater.controlpoints:
for ground_object in cp.ground_objects:
if ground_object.might_have_aa and not ground_object.is_dead:
for g in ground_object.groups:
threat_range = ground_object.threat_range(g)
if not threat_range:
continue
faction = "BlueAA" if cp.captured else "RedAA"
luaData[faction][g.name] = {
"name": ground_object.name,
"range": threat_range.meters,
"position": {
"x": ground_object.position.x,
"y": ground_object.position.y,
},
}
# set a LUA table with data from Liberation that we want to set
# at the moment it contains Liberation's install path, and an overridable definition for the JTACAutoLase function
# later, we'll add data about the units and points having been generated, in order to facilitate the configuration of the plugin lua scripts
state_location = "[[" + os.path.abspath(".") + "]]"
lua = (
"""
-- setting configuration table
env.info("DCSLiberation|: setting configuration table")
-- all data in this table is overridable.
dcsLiberation = {}
-- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory
dcsLiberation.installPath="""
+ state_location
+ """
"""
)
# Process the tankers
lua += """
-- list the tankers generated by Liberation
dcsLiberation.Tankers = {
"""
for key in luaData["Tankers"]:
data = luaData["Tankers"][key]
dcsGroupName = data["dcsGroupName"]
callsign = data["callsign"]
variant = data["variant"]
tacan = data["tacan"]
radio = data["radio"]
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', variant='{variant}', tacan='{tacan}', radio='{radio}' }}, \n"
# lua += f" {{name='{dcsGroupName}', description='{callsign} ({variant})', information='Tacan:{tacan} Radio:{radio}' }}, \n"
lua += "}"
# Process the AWACSes
lua += """
-- list the AWACs generated by Liberation
dcsLiberation.AWACs = {
"""
for key in luaData["AWACs"]:
data = luaData["AWACs"][key]
dcsGroupName = data["dcsGroupName"]
callsign = data["callsign"]
radio = data["radio"]
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', radio='{radio}' }}, \n"
# lua += f" {{name='{dcsGroupName}', description='{callsign} (AWACS)', information='Radio:{radio}' }}, \n"
lua += "}"
# Process the JTACs
lua += """
-- list the JTACs generated by Liberation
dcsLiberation.JTACs = {
"""
for key in luaData["JTACs"]:
data = luaData["JTACs"][key]
dcsGroupName = data["dcsGroupName"]
callsign = data["callsign"]
zone = data["zone"]
laserCode = data["laserCode"]
dcsUnit = data["dcsUnit"]
radio = data["radio"]
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', zone={repr(zone)}, laserCode='{laserCode}', dcsUnit='{dcsUnit}', radio='{radio}' }}, \n"
lua += "}"
# Process the Target Points
lua += """
-- list the target points generated by Liberation
dcsLiberation.TargetPoints = {
"""
for key in luaData["TargetPoints"]:
data = luaData["TargetPoints"][key]
name = data["name"]
pointType = data["type"]
positionX = data["position"]["x"]
positionY = data["position"]["y"]
lua += f" {{name='{name}', pointType='{pointType}', positionX='{positionX}', positionY='{positionY}' }}, \n"
# lua += f" {{name='{pointType} {name}', point{{x={positionX}, z={positionY} }} }}, \n"
lua += "}"
lua += """
-- list the airbases generated by Liberation
-- dcsLiberation.Airbases = {}
-- list the aircraft carriers generated by Liberation
-- dcsLiberation.Carriers = {}
-- list the Red AA generated by Liberation
dcsLiberation.RedAA = {
"""
for key in luaData["RedAA"]:
data = luaData["RedAA"][key]
name = data["name"]
radius = data["range"]
positionX = data["position"]["x"]
positionY = data["position"]["y"]
lua += f" {{dcsGroupName='{key}', name='{name}', range='{radius}', positionX='{positionX}', positionY='{positionY}' }}, \n"
lua += "}"
lua += """
-- list the Blue AA generated by Liberation
dcsLiberation.BlueAA = {
"""
for key in luaData["BlueAA"]:
data = luaData["BlueAA"][key]
name = data["name"]
radius = data["range"]
positionX = data["position"]["x"]
positionY = data["position"]["y"]
lua += f" {{dcsGroupName='{key}', name='{name}', range='{radius}', positionX='{positionX}', positionY='{positionY}' }}, \n"
lua += "}"
lua += """
"""
trigger = TriggerStart(comment="Set DCS Liberation data")
trigger.add_action(DoScript(String(lua)))
Operation.current_mission.triggerrules.triggers.append(trigger)

View File

@ -38,8 +38,8 @@ def _autosave_path() -> str:
return str(save_dir() / "autosave.liberation")
def mission_path_for(name: str) -> str:
return os.path.join(base_path(), "Missions", name)
def mission_path_for(name: str) -> Path:
return Path(base_path()) / "Missions" / name
def load_game(path: str) -> Optional[Game]:

View File

@ -5,12 +5,12 @@ import logging
import textwrap
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional, TYPE_CHECKING, Type
from typing import List, Optional, TYPE_CHECKING
from game.settings import Settings
if TYPE_CHECKING:
from game.operation.operation import Operation
from game.missiongenerator.luagenerator import LuaGenerator
class LuaPluginWorkOrder:
@ -22,11 +22,11 @@ class LuaPluginWorkOrder:
self.mnemonic = mnemonic
self.disable = disable
def work(self, operation: Type[Operation]) -> None:
def work(self, lua_generator: LuaGenerator) -> None:
if self.disable:
operation.bypass_plugin_script(self.mnemonic)
lua_generator.bypass_plugin_script(self.mnemonic)
else:
operation.inject_plugin_script(
lua_generator.inject_plugin_script(
self.parent_mnemonic, self.filename, self.mnemonic
)
@ -151,11 +151,11 @@ class LuaPlugin(PluginSettings):
for option in self.definition.options:
option.set_settings(self.settings)
def inject_scripts(self, operation: Type[Operation]) -> None:
def inject_scripts(self, lua_generator: LuaGenerator) -> None:
for work_order in self.definition.work_orders:
work_order.work(operation)
work_order.work(lua_generator)
def inject_configuration(self, operation: Type[Operation]) -> None:
def inject_configuration(self, lua_generator: LuaGenerator) -> None:
# inject the plugin options
if self.options:
option_decls = []
@ -181,7 +181,9 @@ class LuaPlugin(PluginSettings):
"""
)
operation.inject_lua_trigger(lua, f"{self.identifier} plugin configuration")
lua_generator.inject_lua_trigger(
lua, f"{self.identifier} plugin configuration"
)
for work_order in self.definition.config_work_orders:
work_order.work(operation)
work_order.work(lua_generator)

View File

@ -1,10 +1,11 @@
import json
import logging
from pathlib import Path
from typing import Dict, List, Optional
from typing import Dict, List
from game.settings import Settings
from game.plugins.luaplugin import LuaPlugin
from .luaplugin import LuaPlugin
class LuaPluginManager:

View File

@ -4,8 +4,8 @@ from dataclasses import dataclass
from typing import Optional, Any, TYPE_CHECKING
if TYPE_CHECKING:
from gen.aircraft import FlightData
from gen.airsupport import AirSupport
from game.missiongenerator.aircraftgenerator import FlightData
from game.missiongenerator.airsupport import AirSupport
class RadioChannelAllocator:

View File

@ -19,10 +19,6 @@ from dcs.terrain.terrain import Terrain
from pyproj import CRS, Transformer
from shapely import geometry, ops
from .controlpoint import (
ControlPoint,
MissionTarget,
)
from .frontline import FrontLine
from .landmap import Landmap, load_landmap, poly_contains
from .latlon import LatLon
@ -30,7 +26,8 @@ from .projections import TransverseMercator
from .seasonalconditions import SeasonalConditions
if TYPE_CHECKING:
from . import TheaterGroundObject
from .controlpoint import ControlPoint, MissionTarget
from .theatergroundobject import TheaterGroundObject
@dataclass

View File

@ -1,8 +1,10 @@
"""Maps generated units back to their Liberation types."""
from __future__ import annotations
import itertools
import math
from dataclasses import dataclass
from typing import Dict, Optional, Any, Union, TypeVar, Generic
from typing import Dict, Optional, Any, TYPE_CHECKING, Union, TypeVar, Generic
from dcs.unit import Vehicle, Ship
from dcs.unitgroup import FlyingGroup, VehicleGroup, StaticGroup, ShipGroup, MovingGroup
@ -11,9 +13,11 @@ from game.dcs.groundunittype import GroundUnitType
from game.squadrons import Pilot
from game.theater import Airfield, ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import BuildingGroundObject, SceneryGroundObject
from game.transfers import CargoShip, Convoy, TransferOrder
from game.ato.flight import Flight
if TYPE_CHECKING:
from game.transfers import CargoShip, Convoy, TransferOrder
@dataclass(frozen=True)
class FlyingUnit:

View File

@ -8,8 +8,8 @@ from __future__ import annotations
from dataclasses import dataclass, field
from typing import Dict, Optional, Tuple
from .radios import MHz, RadioFrequency
from .tacan import TacanBand, TacanChannel
from game.radio.radios import MHz, RadioFrequency
from game.radio.tacan import TacanBand, TacanChannel
@dataclass

View File

@ -46,7 +46,6 @@ from game.ato.flightwaypoint import FlightWaypoint
from game.ato.flight import Flight
from .traveltime import GroundSpeed, TravelTime
from .waypointbuilder import StrikeTarget, WaypointBuilder
from ..conflictgen import Conflict, FRONTLINE_LENGTH
if TYPE_CHECKING:
from game.ato.package import Package
@ -1605,7 +1604,13 @@ class FlightPlanBuilder:
if not isinstance(location, FrontLine):
raise InvalidObjectiveLocation(flight.flight_type, location)
ingress, heading, distance = Conflict.frontline_vector(location, self.theater)
from game.missiongenerator.frontlineconflictdescription import (
FrontLineConflictDescription,
)
ingress, heading, distance = FrontLineConflictDescription.frontline_vector(
location, self.theater
)
center = ingress.point_from_heading(heading.degrees, distance / 2)
egress = ingress.point_from_heading(heading.degrees, distance)
@ -1626,6 +1631,8 @@ class FlightPlanBuilder:
patrol_speed = flight.unit_type.preferred_patrol_speed(ingress_egress_altitude)
use_agl_ingress_egress = is_helo
from game.missiongenerator.frontlineconflictdescription import FRONTLINE_LENGTH
return CasFlightPlan(
package=self.package,
flight=flight,

View File

@ -10,8 +10,8 @@ from dcs.terrain.terrain import Airport
from game.weather import Conditions
from game.utils import Heading
from .airfields import AIRFIELD_DATA
from .radios import RadioFrequency
from .tacan import TacanChannel
from game.radio.radios import RadioFrequency
from game.radio.tacan import TacanChannel
@dataclass(frozen=True)

View File

@ -3,7 +3,9 @@ from PySide2.QtGui import QStandardItem, QStandardItemModel
from game import Game
from game.theater import ControlPointType, BuildingGroundObject
from game.utils import Distance
from gen.conflictgen import Conflict
from game.missiongenerator.frontlineconflictdescription import (
FrontLineConflictDescription,
)
from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.flightwaypoint import FlightWaypoint
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
@ -65,7 +67,9 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
if self.include_frontlines:
for front_line in self.game.theater.conflicts():
pos = Conflict.frontline_position(front_line, self.game.theater)[0]
pos = FrontLineConflictDescription.frontline_position(
front_line, self.game.theater
)[0]
wpt = FlightWaypoint(
FlightWaypointType.CUSTOM,
pos.x,

View File

@ -114,7 +114,7 @@ def create_mission(terrain: Terrain) -> Path:
mission_path = persistency.mission_path_for(f"export_{terrain.name.lower()}.miz")
m.save(mission_path)
return Path(mission_path)
return mission_path
def load_coordinate_data(data: Dict[str, Any]) -> Dict[str, Coordinates]:

View File

@ -32,8 +32,7 @@ from typing import Dict, Iterable, Union
import lupa
import game # Needed to resolve cyclic import, for some reason.
from gen.beacons import Beacon, BeaconType, BEACONS_RESOURCE_PATH
from game.missiongenerator.beacons import Beacon, BeaconType, BEACONS_RESOURCE_PATH
THIS_DIR = Path(__file__).parent.resolve()
SRC_DIR = THIS_DIR.parents[1]

View File

@ -1,4 +1,4 @@
from gen.tacan import (
from game.radio.tacan import (
OutOfTacanChannelsError,
TacanBand,
TacanChannel,