diff --git a/game/ato/flighttype.py b/game/ato/flighttype.py index f79ee594..de38f9bf 100644 --- a/game/ato/flighttype.py +++ b/game/ato/flighttype.py @@ -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. diff --git a/game/ato/flightwaypoint.py b/game/ato/flightwaypoint.py index aa511f3b..c7af0aa7 100644 --- a/game/ato/flightwaypoint.py +++ b/game/ato/flightwaypoint.py @@ -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__( diff --git a/game/ato/flightwaypointtype.py b/game/ato/flightwaypointtype.py index 7e47036d..bee6bfdb 100644 --- a/game/ato/flightwaypointtype.py +++ b/game/ato/flightwaypointtype.py @@ -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. """ diff --git a/game/ato/package.py b/game/ato/package.py index 854e4f70..5c509cd2 100644 --- a/game/ato/package.py +++ b/game/ato/package.py @@ -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: diff --git a/game/dcs/aircrafttype.py b/game/dcs/aircrafttype.py index 421e55d5..dea78fec 100644 --- a/game/dcs/aircrafttype.py +++ b/game/dcs/aircrafttype.py @@ -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) diff --git a/game/event/event.py b/game/event/event.py index d38f49dd..80fbe137 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -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: diff --git a/game/game.py b/game/game.py index be7da691..282f8efe 100644 --- a/game/game.py +++ b/game/game.py @@ -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) diff --git a/game/missiongenerator/__init__.py b/game/missiongenerator/__init__.py new file mode 100644 index 00000000..bf896fd8 --- /dev/null +++ b/game/missiongenerator/__init__.py @@ -0,0 +1 @@ +from .missiongenerator import MissionGenerator diff --git a/gen/aircraft.py b/game/missiongenerator/aircraftgenerator.py similarity index 99% rename from gen/aircraft.py rename to game/missiongenerator/aircraftgenerator.py index 9d2cbcee..85224300 100644 --- a/gen/aircraft.py +++ b/game/missiongenerator/aircraftgenerator.py @@ -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 diff --git a/gen/airsupport.py b/game/missiongenerator/airsupport.py similarity index 92% rename from gen/airsupport.py rename to game/missiongenerator/airsupport.py index fa189376..e2c61fa0 100644 --- a/gen/airsupport.py +++ b/game/missiongenerator/airsupport.py @@ -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 diff --git a/gen/airsupportgen.py b/game/missiongenerator/airsupportgenerator.py similarity index 94% rename from gen/airsupportgen.py rename to game/missiongenerator/airsupportgenerator.py index 8478eb89..7c22bb12 100644 --- a/gen/airsupportgen.py +++ b/game/missiongenerator/airsupportgenerator.py @@ -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, diff --git a/gen/beacons.py b/game/missiongenerator/beacons.py similarity index 95% rename from gen/beacons.py rename to game/missiongenerator/beacons.py index b54eacb1..475bd2c5 100644 --- a/gen/beacons.py +++ b/game/missiongenerator/beacons.py @@ -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") diff --git a/gen/briefinggen.py b/game/missiongenerator/briefinggenerator.py similarity index 95% rename from gen/briefinggen.py rename to game/missiongenerator/briefinggenerator.py index 02b91bb8..7f6c2962 100644 --- a/gen/briefinggen.py +++ b/game/missiongenerator/briefinggenerator.py @@ -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: diff --git a/gen/cargoshipgen.py b/game/missiongenerator/cargoshipgenerator.py similarity index 100% rename from gen/cargoshipgen.py rename to game/missiongenerator/cargoshipgenerator.py diff --git a/gen/convoygen.py b/game/missiongenerator/convoygenerator.py similarity index 100% rename from gen/convoygen.py rename to game/missiongenerator/convoygenerator.py diff --git a/gen/environmentgen.py b/game/missiongenerator/environmentgenerator.py similarity index 100% rename from gen/environmentgen.py rename to game/missiongenerator/environmentgenerator.py diff --git a/gen/armor.py b/game/missiongenerator/flotgenerator.py similarity index 98% rename from gen/armor.py rename to game/missiongenerator/flotgenerator.py index 6880e1c4..9ef91a8f 100644 --- a/gen/armor.py +++ b/game/missiongenerator/flotgenerator.py @@ -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 ) diff --git a/gen/forcedoptionsgen.py b/game/missiongenerator/forcedoptionsgenerator.py similarity index 100% rename from gen/forcedoptionsgen.py rename to game/missiongenerator/forcedoptionsgenerator.py diff --git a/gen/conflictgen.py b/game/missiongenerator/frontlineconflictdescription.py similarity index 98% rename from gen/conflictgen.py rename to game/missiongenerator/frontlineconflictdescription.py index 6693367e..d4e0c9bf 100644 --- a/gen/conflictgen.py +++ b/game/missiongenerator/frontlineconflictdescription.py @@ -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( diff --git a/gen/kneeboard.py b/game/missiongenerator/kneeboard.py similarity index 99% rename from gen/kneeboard.py rename to game/missiongenerator/kneeboard.py index 23519c38..f7a8f227 100644 --- a/gen/kneeboard.py +++ b/game/missiongenerator/kneeboard.py @@ -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 diff --git a/gen/lasercoderegistry.py b/game/missiongenerator/lasercoderegistry.py similarity index 100% rename from gen/lasercoderegistry.py rename to game/missiongenerator/lasercoderegistry.py diff --git a/game/missiongenerator/luagenerator.py b/game/missiongenerator/luagenerator.py new file mode 100644 index 00000000..af0045a4 --- /dev/null +++ b/game/missiongenerator/luagenerator.py @@ -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) diff --git a/game/missiongenerator/missiongenerator.py b/game/missiongenerator/missiongenerator.py new file mode 100644 index 00000000..63720422 --- /dev/null +++ b/game/missiongenerator/missiongenerator.py @@ -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 diff --git a/gen/groundobjectsgen.py b/game/missiongenerator/tgogenerator.py similarity index 98% rename from gen/groundobjectsgen.py rename to game/missiongenerator/tgogenerator.py index 84ef0509..dcc4b4d9 100644 --- a/gen/groundobjectsgen.py +++ b/game/missiongenerator/tgogenerator.py @@ -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 diff --git a/gen/triggergen.py b/game/missiongenerator/triggergenerator.py similarity index 99% rename from gen/triggergen.py rename to game/missiongenerator/triggergenerator.py index 2a70d204..3983b798 100644 --- a/gen/triggergen.py +++ b/game/missiongenerator/triggergenerator.py @@ -47,7 +47,7 @@ class Silence(Option): Key = 7 -class TriggersGenerator: +class TriggerGenerator: capture_zone_types = (Fob,) capture_zone_flag = 600 diff --git a/gen/visualgen.py b/game/missiongenerator/visualsgenerator.py similarity index 88% rename from gen/visualgen.py rename to game/missiongenerator/visualsgenerator.py index 3a11652e..ed43a9f8 100644 --- a/gen/visualgen.py +++ b/game/missiongenerator/visualsgenerator.py @@ -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() diff --git a/game/operation/operation.py b/game/operation/operation.py deleted file mode 100644 index 9770a4e7..00000000 --- a/game/operation/operation.py +++ /dev/null @@ -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) diff --git a/game/persistency.py b/game/persistency.py index 7685dd09..4520b021 100644 --- a/game/persistency.py +++ b/game/persistency.py @@ -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]: diff --git a/game/plugins/luaplugin.py b/game/plugins/luaplugin.py index 5799c748..2648c9b7 100644 --- a/game/plugins/luaplugin.py +++ b/game/plugins/luaplugin.py @@ -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) diff --git a/game/plugins/manager.py b/game/plugins/manager.py index 7b6eabba..5ffdf9de 100644 --- a/game/plugins/manager.py +++ b/game/plugins/manager.py @@ -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: diff --git a/game/operation/__init__.py b/game/radio/__init__.py similarity index 100% rename from game/operation/__init__.py rename to game/radio/__init__.py diff --git a/game/radio/channels.py b/game/radio/channels.py index 214596fe..04e2d8f5 100644 --- a/game/radio/channels.py +++ b/game/radio/channels.py @@ -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: diff --git a/gen/radios.py b/game/radio/radios.py similarity index 100% rename from gen/radios.py rename to game/radio/radios.py diff --git a/gen/tacan.py b/game/radio/tacan.py similarity index 100% rename from gen/tacan.py rename to game/radio/tacan.py diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index 7cf3fcd2..d6ace0fb 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -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 diff --git a/game/unitmap.py b/game/unitmap.py index 54b285c0..d2f85949 100644 --- a/game/unitmap.py +++ b/game/unitmap.py @@ -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: diff --git a/gen/airfields.py b/gen/airfields.py index 7998cbf4..38ad2ee1 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -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 diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 427dc2bf..dc7ea3fe 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -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, diff --git a/gen/runways.py b/gen/runways.py index ef9ab52f..0f30fdcc 100644 --- a/gen/runways.py +++ b/gen/runways.py @@ -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) diff --git a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py index ff78c7ca..f857dfd6 100644 --- a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py +++ b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py @@ -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, diff --git a/resources/tools/export_coordinates.py b/resources/tools/export_coordinates.py index e59c90d0..0328d75d 100644 --- a/resources/tools/export_coordinates.py +++ b/resources/tools/export_coordinates.py @@ -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]: diff --git a/resources/tools/import_beacons.py b/resources/tools/import_beacons.py index 9342f9a1..7b5c8762 100644 --- a/resources/tools/import_beacons.py +++ b/resources/tools/import_beacons.py @@ -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] diff --git a/tests/test_tacan.py b/tests/test_tacan.py index d1afa346..d01fa9f6 100644 --- a/tests/test_tacan.py +++ b/tests/test_tacan.py @@ -1,4 +1,4 @@ -from gen.tacan import ( +from game.radio.tacan import ( OutOfTacanChannelsError, TacanBand, TacanChannel,