diff --git a/game/__init__.py b/game/__init__.py index 126a7d24..c651b19d 100644 --- a/game/__init__.py +++ b/game/__init__.py @@ -1,2 +1,3 @@ from .game import Game -from . import db \ No newline at end of file +from . import db +from .version import VERSION diff --git a/game/data/building_data.py b/game/data/building_data.py index 939609a6..ab2555c3 100644 --- a/game/data/building_data.py +++ b/game/data/building_data.py @@ -1,11 +1,11 @@ import inspect import dcs -DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick', 'aa'] +DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick'] -WW2_FREE = ['fuel', 'factory', 'ware', 'aa'] -WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp', 'aa'] -WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'aa'] +WW2_FREE = ['fuel', 'factory', 'ware'] +WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp'] +WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp'] FORTIFICATION_BUILDINGS = ['Siegfried Line', 'Concertina wire', 'Concertina Wire', 'Czech hedgehogs 1', 'Czech hedgehogs 2', 'Dragonteeth 1', 'Dragonteeth 2', 'Dragonteeth 3', 'Dragonteeth 4', 'Dragonteeth 5', diff --git a/game/event/event.py b/game/event/event.py index e6b7d142..a45b64eb 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -14,7 +14,6 @@ from game.infos.information import Information from game.operation.operation import Operation from gen.ground_forces.combat_stance import CombatStance from theater import ControlPoint -from theater.start_generator import generate_airbase_defense_group if TYPE_CHECKING: from ..game import Game diff --git a/game/version.py b/game/version.py new file mode 100644 index 00000000..e173526f --- /dev/null +++ b/game/version.py @@ -0,0 +1,8 @@ +from pathlib import Path + + +#: Current version of Liberation. +VERSION = "2.2.0-preview" +if Path("buildnumber").exists(): + with open("buildnumber", "r") as file: + VERSION += f"-{file.readline()}" diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index 779492e0..e0e5c1ee 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -1,8 +1,18 @@ +"""Generators for creating the groups for ground objectives. + +The classes in this file are responsible for creating the vehicle groups, ship +groups, statics, missile sites, and AA sites for the mission. Each of these +objectives is defined in the Theater by a TheaterGroundObject. These classes +create the pydcs groups and statics for those areas and add them to the mission. +""" +from __future__ import annotations + import logging import random -from typing import Dict, Iterator +from typing import Dict, Iterator, Optional, TYPE_CHECKING from dcs import Mission +from dcs.country import Country from dcs.statics import fortification_map, warehouse_map from dcs.task import ( ActivateBeaconCommand, @@ -10,22 +20,339 @@ from dcs.task import ( EPLRS, OptAlarmState, ) -from dcs.unit import Ship, Vehicle -from dcs.unitgroup import StaticGroup +from dcs.unit import Ship, Vehicle, Unit +from dcs.unitgroup import Group, ShipGroup, StaticGroup +from dcs.unittype import StaticType, UnitType from game import db from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID from game.db import unit_type_from_name +from theater import ControlPoint, TheaterGroundObject +from theater.theatergroundobject import ( + BuildingGroundObject, CarrierGroundObject, + GenericCarrierGroundObject, + LhaGroundObject, ShipGroundObject, +) from .conflictgen import Conflict -from .radios import RadioRegistry +from .radios import RadioFrequency, RadioRegistry from .runways import RunwayData -from .tacan import TacanBand, TacanRegistry +from .tacan import TacanBand, TacanChannel, TacanRegistry + +if TYPE_CHECKING: + from game import Game + FARP_FRONTLINE_DISTANCE = 10000 AA_CP_MIN_DISTANCE = 40000 +class GenericGroundObjectGenerator: + """An unspecialized ground object generator. + + Currently used only for SAM and missile (V1/V2) sites. + """ + def __init__(self, ground_object: TheaterGroundObject, country: Country, + game: Game, mission: Mission) -> None: + self.ground_object = ground_object + self.country = country + self.game = game + self.m = mission + + def generate(self) -> None: + if self.game.position_culled(self.ground_object.position): + return + + for group in self.ground_object.groups: + if not group.units: + logging.warning(f"Found empty group in {self.ground_object}") + continue + + unit_type = unit_type_from_name(group.units[0].type) + if unit_type is None: + raise RuntimeError( + f"Unrecognized unit type: {group.units[0].type}") + + vg = self.m.vehicle_group(self.country, group.name, unit_type, + position=group.position, + heading=group.units[0].heading) + vg.units[0].name = self.m.string(group.units[0].name) + vg.units[0].player_can_drive = True + for i, u in enumerate(group.units): + if i > 0: + vehicle = Vehicle(self.m.next_unit_id(), + self.m.string(u.name), u.type) + vehicle.position.x = u.position.x + vehicle.position.y = u.position.y + vehicle.heading = u.heading + vehicle.player_can_drive = True + vg.add_unit(vehicle) + + self.enable_eplrs(vg, unit_type) + self.set_alarm_state(vg) + + @staticmethod + def enable_eplrs(group: Group, unit_type: UnitType) -> None: + if hasattr(unit_type, 'eplrs'): + if unit_type.eplrs: + group.points[0].tasks.append(EPLRS(group.id)) + + def set_alarm_state(self, group: Group) -> None: + if self.game.settings.perf_red_alert_state: + group.points[0].tasks.append(OptAlarmState(2)) + else: + group.points[0].tasks.append(OptAlarmState(1)) + + +class BuildingSiteGenerator(GenericGroundObjectGenerator): + """Generator for building sites. + + Building sites are the primary type of non-airbase objective locations that + appear on the map. They come in a handful of variants each with different + types of buildings and ground units. + """ + + def generate(self) -> None: + if self.game.position_culled(self.ground_object.position): + return + + if self.ground_object.dcs_identifier in warehouse_map: + static_type = warehouse_map[self.ground_object.dcs_identifier] + self.generate_static(static_type) + elif self.ground_object.dcs_identifier in fortification_map: + static_type = fortification_map[self.ground_object.dcs_identifier] + self.generate_static(static_type) + elif self.ground_object.dcs_identifier in FORTIFICATION_UNITS_ID: + for f in FORTIFICATION_UNITS: + if f.id == self.ground_object.dcs_identifier: + unit_type = f + self.generate_vehicle_group(unit_type) + break + else: + logging.error( + f"{self.ground_object.dcs_identifier} not found in static maps") + + def generate_vehicle_group(self, unit_type: UnitType) -> None: + if not self.ground_object.is_dead: + self.m.vehicle_group( + country=self.country, + name=self.ground_object.string_identifier, + _type=unit_type, + position=self.ground_object.position, + heading=self.ground_object.heading, + ) + + def generate_static(self, static_type: StaticType) -> None: + self.m.static_group( + country=self.country, + name=self.ground_object.string_identifier, + _type=static_type, + position=self.ground_object.position, + heading=self.ground_object.heading, + dead=self.ground_object.is_dead, + ) + + +class GenericCarrierGenerator(GenericGroundObjectGenerator): + """Base type for carrier group generation. + + Used by both CV(N) groups and LHA groups. + """ + def __init__(self, ground_object: GenericCarrierGroundObject, + control_point: ControlPoint, country: Country, game: Game, + mission: Mission, radio_registry: RadioRegistry, + tacan_registry: TacanRegistry, icls_alloc: Iterator[int], + runways: Dict[str, RunwayData]) -> None: + super().__init__(ground_object, country, game, mission) + self.ground_object = ground_object + self.control_point = control_point + self.radio_registry = radio_registry + self.tacan_registry = tacan_registry + self.icls_alloc = icls_alloc + self.runways = runways + + def generate(self) -> None: + # TODO: Require single group? + for group in self.ground_object.groups: + if not group.units: + logging.warning( + f"Found empty carrier group in {self.control_point}") + continue + + atc = self.radio_registry.alloc_uhf() + ship_group = self.configure_carrier(group, atc) + for unit in group.units[1:]: + ship_group.add_unit(self.create_ship(unit, atc)) + + tacan = self.tacan_registry.alloc_for_band(TacanBand.X) + tacan_callsign = self.tacan_callsign() + icls = next(self.icls_alloc) + + brc = self.steam_into_wind(ship_group) + self.activate_beacons(ship_group, tacan, tacan_callsign, icls) + self.add_runway_data(brc or 0, atc, tacan, tacan_callsign, icls) + + def get_carrier_type(self, group: Group) -> UnitType: + unit_type = unit_type_from_name(group.units[0].type) + if unit_type is None: + raise RuntimeError( + f"Unrecognized carrier name: {group.units[0].type}") + return unit_type + + def configure_carrier(self, group: Group, + atc_channel: RadioFrequency) -> ShipGroup: + unit_type = self.get_carrier_type(group) + + ship_group = self.m.ship_group(self.country, group.name, unit_type, + position=group.position, + heading=group.units[0].heading) + ship_group.set_frequency(atc_channel.hertz) + ship_group.units[0].name = self.m.string(group.units[0].name) + return ship_group + + def create_ship(self, unit: Unit, atc_channel: RadioFrequency) -> Ship: + ship = Ship(self.m.next_unit_id(), + self.m.string(unit.name), + unit_type_from_name(unit.type)) + ship.position.x = unit.position.x + ship.position.y = unit.position.y + ship.heading = unit.heading + # TODO: Verify. + ship.set_frequency(atc_channel.hertz) + return ship + + def steam_into_wind(self, group: ShipGroup) -> Optional[int]: + brc = self.m.weather.wind_at_ground.direction + 180 + for attempt in range(5): + point = group.points[0].position.point_from_heading( + brc, 100000 - attempt * 20000) + if self.game.theater.is_in_sea(point): + group.add_waypoint(point) + return brc + return None + + def tacan_callsign(self) -> str: + raise NotImplementedError + + @staticmethod + def activate_beacons(group: ShipGroup, tacan: TacanChannel, + callsign: str, icls: int) -> None: + group.points[0].tasks.append(ActivateBeaconCommand( + channel=tacan.number, + modechannel=tacan.band.value, + callsign=callsign, + unit_id=group.units[0].id, + aa=False + )) + group.points[0].tasks.append(ActivateICLSCommand( + icls, unit_id=group.units[0].id + )) + + def add_runway_data(self, brc: int, atc: RadioFrequency, + tacan: TacanChannel, callsign: str, icls: int) -> None: + # TODO: Make unit name usable. + # This relies on one control point mapping exactly + # to one LHA, carrier, or other usable "runway". + # This isn't wholly true, since the DD escorts of + # the carrier group are valid for helicopters, but + # they aren't exposed as such to the game. Should + # clean this up so that's possible. We can't use the + # unit name since it's an arbitrary ID. + self.runways[self.control_point.name] = RunwayData( + self.control_point.name, + brc, + "N/A", + atc=atc, + tacan=tacan, + tacan_callsign=callsign, + icls=icls, + ) + + +class CarrierGenerator(GenericCarrierGenerator): + """Generator for CV(N) groups.""" + + def get_carrier_type(self, group: Group) -> UnitType: + unit_type = super().get_carrier_type(group) + if self.game.settings.supercarrier: + unit_type = db.upgrade_to_supercarrier(unit_type, + self.control_point.name) + return unit_type + + def tacan_callsign(self) -> str: + # TODO: Assign these properly. + return random.choice([ + "STE", + "CVN", + "CVH", + "CCV", + "ACC", + "ARC", + "GER", + "ABR", + "LIN", + "TRU", + ]) + + +class LhaGenerator(GenericCarrierGenerator): + """Generator for LHA groups.""" + + def tacan_callsign(self) -> str: + # TODO: Assign these properly. + return random.choice([ + "LHD", + "LHA", + "LHB", + "LHC", + "LHD", + "LDS", + ]) + + +class ShipObjectGenerator(GenericGroundObjectGenerator): + """Generator for non-carrier naval groups.""" + + def generate(self) -> None: + if self.game.position_culled(self.ground_object.position): + return + + for group in self.ground_object.groups: + if not group.units: + logging.warning(f"Found empty group in {self.ground_object}") + continue + + unit_type = unit_type_from_name(group.units[0].type) + if unit_type is None: + raise RuntimeError( + f"Unrecognized unit type: {group.units[0].type}") + + self.generate_group(group, unit_type) + + def generate_group(self, group_def: Group, unit_type: UnitType): + group = self.m.ship_group(self.country, group_def.name, unit_type, + position=group_def.position, + heading=group_def.units[0].heading) + group.units[0].name = self.m.string(group_def.units[0].name) + # TODO: Skipping the first unit looks like copy pasta from the carrier. + for unit in group_def.units[1:]: + unit_type = unit_type_from_name(unit.type) + ship = Ship(self.m.next_unit_id(), + self.m.string(unit.name), unit_type) + ship.position.x = unit.position.x + ship.position.y = unit.position.y + ship.heading = unit.heading + group.add_unit(ship) + self.set_alarm_state(group) + + class GroundObjectsGenerator: + """Creates DCS groups and statics for the theater during mission generation. + + Most of the work of group/static generation is delegated to the other + generator classes. This class is responsible for finding each of the + locations for spawning ground objects, determining their types, and creating + the appropriate generators. + """ FARP_CAPACITY = 4 def __init__(self, mission: Mission, conflict: Conflict, game, @@ -61,188 +388,34 @@ class GroundObjectsGenerator: ) def generate(self): - for cp in self.game.theater.controlpoints: - if cp.captured: - country = self.game.player_country + country_name = self.game.player_country else: - country = self.game.enemy_country - side = self.m.country(country) + country_name = self.game.enemy_country + country = self.m.country(country_name) for ground_object in cp.ground_objects: - if ground_object.dcs_identifier == "AA": - - if self.game.position_culled(ground_object.position): - continue - - for g in ground_object.groups: - if len(g.units) > 0: - utype = unit_type_from_name(g.units[0].type) - - if not ground_object.sea_object: - vg = self.m.vehicle_group(side, g.name, utype, position=g.position, heading=g.units[0].heading) - vg.units[0].name = self.m.string(g.units[0].name) - vg.units[0].player_can_drive = True - for i, u in enumerate(g.units): - if i > 0: - vehicle = Vehicle(self.m.next_unit_id(), self.m.string(u.name), u.type) - vehicle.position.x = u.position.x - vehicle.position.y = u.position.y - vehicle.heading = u.heading - vehicle.player_can_drive = True - vg.add_unit(vehicle) - - if hasattr(utype, 'eplrs'): - if utype.eplrs: - vg.points[0].tasks.append(EPLRS(vg.id)) - else: - vg = self.m.ship_group(side, g.name, utype, position=g.position, - heading=g.units[0].heading) - vg.units[0].name = self.m.string(g.units[0].name) - for i, u in enumerate(g.units): - utype = unit_type_from_name(u.type) - if i > 0: - ship = Ship(self.m.next_unit_id(), self.m.string(u.name), utype) - ship.position.x = u.position.x - ship.position.y = u.position.y - ship.heading = u.heading - vg.add_unit(ship) - - if self.game.settings.perf_red_alert_state: - vg.points[0].tasks.append(OptAlarmState(2)) - else: - vg.points[0].tasks.append(OptAlarmState(1)) - - - elif ground_object.dcs_identifier in ["CARRIER", "LHA"]: - for g in ground_object.groups: - if len(g.units) > 0: - - utype = unit_type_from_name(g.units[0].type) - if ground_object.dcs_identifier == "CARRIER" and self.game.settings.supercarrier == True: - utype = db.upgrade_to_supercarrier(utype, cp.name) - - sg = self.m.ship_group(side, g.name, utype, position=g.position, heading=g.units[0].heading) - atc_channel = self.radio_registry.alloc_uhf() - sg.set_frequency(atc_channel.hertz) - sg.units[0].name = self.m.string(g.units[0].name) - - for i, u in enumerate(g.units): - if i > 0: - ship = Ship(self.m.next_unit_id(), self.m.string(u.name), unit_type_from_name(u.type)) - ship.position.x = u.position.x - ship.position.y = u.position.y - ship.heading = u.heading - # TODO: Verify. - ship.set_frequency(atc_channel.hertz) - sg.add_unit(ship) - - # Find carrier direction (In the wind) - found_carrier_destination = False - attempt = 0 - brc = self.m.weather.wind_at_ground.direction + 180 - while not found_carrier_destination and attempt < 5: - point = sg.points[0].position.point_from_heading(brc, 100000-attempt*20000) - if self.game.theater.is_in_sea(point): - found_carrier_destination = True - sg.add_waypoint(point) - else: - attempt = attempt + 1 - - # Set UP TACAN and ICLS - tacan = self.tacan_registry.alloc_for_band(TacanBand.X) - icls_channel = next(self.icls_alloc) - # TODO: Assign these properly. - if ground_object.dcs_identifier == "CARRIER": - tacan_callsign = random.choice([ - "STE", - "CVN", - "CVH", - "CCV", - "ACC", - "ARC", - "GER", - "ABR", - "LIN", - "TRU", - ]) - else: - tacan_callsign = random.choice([ - "LHD", - "LHA", - "LHB", - "LHC", - "LHD", - "LDS", - ]) - sg.points[0].tasks.append(ActivateBeaconCommand( - channel=tacan.number, - modechannel=tacan.band.value, - callsign=tacan_callsign, - unit_id=sg.units[0].id, - aa=False - )) - sg.points[0].tasks.append(ActivateICLSCommand( - icls_channel, - unit_id=sg.units[0].id - )) - # TODO: Make unit name usable. - # This relies on one control point mapping exactly - # to one LHA, carrier, or other usable "runway". - # This isn't wholly true, since the DD escorts of - # the carrier group are valid for helicopters, but - # they aren't exposed as such to the game. Should - # clean this up so that's possible. We can't use the - # unit name since it's an arbitrary ID. - self.runways[cp.name] = RunwayData( - cp.name, - brc, - "N/A", - atc=atc_channel, - tacan=tacan, - tacan_callsign=tacan_callsign, - icls=icls_channel, - ) - + if isinstance(ground_object, BuildingGroundObject): + generator = BuildingSiteGenerator(ground_object, country, + self.game, self.m) + elif isinstance(ground_object, CarrierGroundObject): + generator = CarrierGenerator(ground_object, cp, country, + self.game, self.m, + self.radio_registry, + self.tacan_registry, + self.icls_alloc, self.runways) + elif isinstance(ground_object, LhaGroundObject): + generator = CarrierGenerator(ground_object, cp, country, + self.game, self.m, + self.radio_registry, + self.tacan_registry, + self.icls_alloc, self.runways) + elif isinstance(ground_object, ShipGroundObject): + generator = ShipObjectGenerator(ground_object, country, + self.game, self.m) else: - - if self.game.position_culled(ground_object.position): - continue - - static_type = None - if ground_object.dcs_identifier in warehouse_map: - static_type = warehouse_map[ground_object.dcs_identifier] - elif ground_object.dcs_identifier in fortification_map: - static_type = fortification_map[ground_object.dcs_identifier] - elif ground_object.dcs_identifier in FORTIFICATION_UNITS_ID: - for f in FORTIFICATION_UNITS: - if f.id == ground_object.dcs_identifier: - unit_type = f - break - else: - print("Didn't find {} in static _map(s)!".format(ground_object.dcs_identifier)) - continue - - if static_type is None: - if not ground_object.is_dead: - group = self.m.vehicle_group( - country=side, - name=ground_object.string_identifier, - _type=unit_type, - position=ground_object.position, - heading=ground_object.heading, - ) - logging.info("generated {}object identifier {} with mission id {}".format( - "dead " if ground_object.is_dead else "", group.name, group.id)) - else: - group = self.m.static_group( - country=side, - name=ground_object.string_identifier, - _type=static_type, - position=ground_object.position, - heading=ground_object.heading, - dead=ground_object.is_dead, - ) - - logging.info("generated {}object identifier {} with mission id {}".format("dead " if ground_object.is_dead else "", group.name, group.id)) \ No newline at end of file + generator = GenericGroundObjectGenerator(ground_object, + country, self.game, + self.m) + generator.generate() diff --git a/gen/sam/group_generator.py b/gen/sam/group_generator.py index 5f6e90d6..f35f1a0e 100644 --- a/gen/sam/group_generator.py +++ b/gen/sam/group_generator.py @@ -15,7 +15,12 @@ if TYPE_CHECKING: from game.game import Game -class GroupGenerator(): +# TODO: Generate a group description rather than a pydcs group. +# It appears that all of this work gets redone at miz generation time (see +# groundobjectsgen for an example). We can do less work and include the data we +# care about in the format we want if we just generate our own group description +# types rather than pydcs groups. +class GroupGenerator: def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Optional[Faction] = None): # faction is not mandatory because some subclasses do not use it self.game = game @@ -34,7 +39,7 @@ class GroupGenerator(): def generate(self): raise NotImplementedError - def get_generated_group(self): + def get_generated_group(self) -> unitgroup.VehicleGroup: return self.vg def add_unit(self, unit_type: VehicleType, name: str, pos_x: float, pos_y: float, heading: int): diff --git a/gen/sam/sam_group_generator.py b/gen/sam/sam_group_generator.py index ea9cb1f8..6acc2759 100644 --- a/gen/sam/sam_group_generator.py +++ b/gen/sam/sam_group_generator.py @@ -1,9 +1,10 @@ import random -from typing import List, Type +from typing import List, Optional, Type from dcs.vehicles import AirDefence +from dcs.unitgroup import VehicleGroup -from game import db +from game import Game, db from gen.sam.aaa_bofors import BoforsGenerator from gen.sam.aaa_flak import FlakGenerator from gen.sam.aaa_flak18 import Flak18Generator @@ -34,6 +35,7 @@ from gen.sam.sam_zsu23 import ZSU23Generator from gen.sam.sam_zu23 import ZU23Generator from gen.sam.sam_zu23_ural import ZU23UralGenerator from gen.sam.sam_zu23_ural_insurgent import ZU23UralInsurgentGenerator +from theater import TheaterGroundObject SAM_MAP = { "HawkGenerator": HawkGenerator, @@ -111,13 +113,14 @@ def get_faction_possible_sams_generator(faction: str) -> List[Type[GroupGenerato return [SAM_MAP[s] for s in db.FACTIONS[faction].sams if s in SAM_MAP.keys()] -def generate_anti_air_group(game, parent_cp, ground_object, faction: str): +def generate_anti_air_group(game: Game, ground_object: TheaterGroundObject, + faction: str) -> Optional[VehicleGroup]: """ This generate a SAM group - :param parentCp: The parent control point - :param ground_object: The ground object which will own the sam group - :param country: Owner country - :return: Nothing, but put the group reference inside the ground object + :param game: The Game. + :param ground_object: The ground object which will own the sam group. + :param faction: Owner faction. + :return: The generated group, or None if one could not be generated. """ possible_sams_generators = get_faction_possible_sams_generator(faction) if len(possible_sams_generators) > 0: @@ -128,7 +131,8 @@ def generate_anti_air_group(game, parent_cp, ground_object, faction: str): return None -def generate_shorad_group(game, parent_cp, ground_object, faction_name: str): +def generate_shorad_group(game: Game, ground_object: TheaterGroundObject, + faction_name: str) -> Optional[VehicleGroup]: faction = db.FACTIONS[faction_name] if len(faction.shorads) > 0: @@ -137,4 +141,4 @@ def generate_shorad_group(game, parent_cp, ground_object, faction_name: str): generator.generate() return generator.get_generated_group() else: - return generate_anti_air_group(game, parent_cp, ground_object, faction_name) + return generate_anti_air_group(game, ground_object, faction_name) diff --git a/qt_ui/main.py b/qt_ui/main.py index c638e476..de179f6b 100644 --- a/qt_ui/main.py +++ b/qt_ui/main.py @@ -7,7 +7,7 @@ from PySide2 import QtWidgets from PySide2.QtGui import QPixmap from PySide2.QtWidgets import QApplication, QSplashScreen -from game import db, persistency +from game import db, persistency, VERSION from qt_ui import ( liberation_install, liberation_theme, @@ -20,7 +20,7 @@ from qt_ui.windows.preferences.QLiberationFirstStartWindow import \ QLiberationFirstStartWindow # Logging setup -logging_config.init_logging(uiconstants.VERSION_STRING) +logging_config.init_logging(VERSION) if __name__ == "__main__": # Load eagerly to catch errors early. diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py index 734006e8..b256705d 100644 --- a/qt_ui/uiconstants.py +++ b/qt_ui/uiconstants.py @@ -7,10 +7,6 @@ from PySide2.QtGui import QColor, QFont, QPixmap from theater.theatergroundobject import CATEGORY_MAP from .liberation_theme import get_theme_icons -VERSION_STRING = "2.2.0-preview" -if Path("buildnumber").exists(): - with open("buildnumber", "r") as file: - VERSION_STRING += f"-{file.readline()}" URLS : Dict[str, str] = { "Manual": "https://github.com/khopa/dcs_liberation/wiki", diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index f1b69e9e..45520c5c 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -1,7 +1,6 @@ import logging -import sys import webbrowser -from typing import Optional, Union +from typing import Optional from PySide2.QtCore import Qt from PySide2.QtGui import QCloseEvent, QIcon @@ -10,14 +9,14 @@ from PySide2.QtWidgets import ( QActionGroup, QDesktopWidget, QFileDialog, QMainWindow, - QMenu, QMessageBox, + QMessageBox, QSplitter, QVBoxLayout, QWidget, ) import qt_ui.uiconstants as CONST -from game import Game, persistency +from game import Game, persistency, VERSION from qt_ui.dialogs import Dialog from qt_ui.displayoptions import DisplayGroup, DisplayOptions, DisplayRule from qt_ui.models import GameModel @@ -47,7 +46,7 @@ class QLiberationWindow(QMainWindow): self.setGame(persistency.restore_game()) self.setGeometry(300, 100, 270, 100) - self.setWindowTitle("DCS Liberation - v" + CONST.VERSION_STRING) + self.setWindowTitle(f"DCS Liberation - v{VERSION}") self.setWindowIcon(QIcon("./resources/icon.png")) self.statusBar().showMessage('Ready') @@ -228,7 +227,7 @@ class QLiberationWindow(QMainWindow): self.liberation_map.setGame(game) def showAboutDialog(self): - text = "
DCS Liberation was originally developed by shdwp, DCS Liberation 2.0 is a partial rewrite based on this work by Khopa." \ diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py index 0a42e7db..0295f6cd 100644 --- a/qt_ui/windows/newgame/QNewGameWizard.py +++ b/qt_ui/windows/newgame/QNewGameWizard.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import datetime import logging from typing import List, Optional @@ -10,15 +9,14 @@ from PySide2.QtWidgets import QVBoxLayout from dcs.task import CAP, CAS import qt_ui.uiconstants as CONST -from game import Game, db +from game import db from game.settings import Settings -from gen import namegen from qt_ui.windows.newgame.QCampaignList import ( Campaign, QCampaignList, load_campaigns, ) -from theater import ConflictTheater, start_generator +from theater.start_generator import GameGenerator class NewGameWizard(QtWidgets.QWizard): @@ -76,39 +74,13 @@ class NewGameWizard(QtWidgets.QWizard): settings.do_not_generate_player_navy = no_player_navy settings.do_not_generate_enemy_navy = no_enemy_navy - self.generatedGame = self.start_new_game(player_name, enemy_name, conflictTheater, midGame, multiplier, - timePeriod, settings, starting_money) + generator = GameGenerator(player_name, enemy_name, conflictTheater, + settings, timePeriod, starting_money, + multiplier, midGame) + self.generatedGame = generator.generate() super(NewGameWizard, self).accept() - def start_new_game(self, player_name: str, enemy_name: str, conflictTheater: ConflictTheater, - midgame: bool, multiplier: float, period: datetime, settings:Settings, starting_money: int): - - # Reset name generator - namegen.reset() - start_generator.prepare_theater(conflictTheater, settings, midgame) - - print("-- Starting New Game Generator") - print("Enemy name : " + enemy_name) - print("Player name : " + player_name) - print("Midgame : " + str(midgame)) - start_generator.generate_initial_units(conflictTheater, enemy_name, True, multiplier) - - print("-- Initial units generated") - game = Game(player_name=player_name, - enemy_name=enemy_name, - theater=conflictTheater, - start_date=period, - settings=settings) - - print("-- Game Object generated") - start_generator.generate_groundobjects(conflictTheater, game) - game.budget = starting_money - game.settings.multiplier = multiplier - game.settings.sams = True - game.settings.version = CONST.VERSION_STRING - return game - class IntroPage(QtWidgets.QWizardPage): def __init__(self, parent=None): diff --git a/theater/controlpoint.py b/theater/controlpoint.py index 96f2c060..4aaedf21 100644 --- a/theater/controlpoint.py +++ b/theater/controlpoint.py @@ -35,7 +35,6 @@ class ControlPoint(MissionTarget): position = None # type: Point name = None # type: str - allow_sea_units = True captured = False has_frontline = True @@ -47,10 +46,9 @@ class ControlPoint(MissionTarget): at: db.StartingPosition, radials: List[int], size: int, importance: float, has_frontline=True, cptype=ControlPointType.AIRBASE): + super().__init__(" ".join(re.split(r" |-", name)[:2]), position) self.id = id - self.name = " ".join(re.split(r" |-", name)[:2]) self.full_name = name - self.position: Point = position self.at = at self.ground_objects: List[TheaterGroundObject] = [] @@ -228,10 +226,8 @@ class ControlPoint(MissionTarget): # Handle cyclic dependency. from .start_generator import generate_airbase_defense_group - airbase_def_id = 0 - for ground_object in self.ground_objects: + for idx, ground_object in enumerate(self.ground_objects): ground_object.groups = [] if ground_object.airbase_group and faction_name != "": - generate_airbase_defense_group(airbase_def_id, ground_object, - faction_name, game, self) - airbase_def_id = airbase_def_id + 1 + generate_airbase_defense_group(idx, ground_object, + faction_name, game) diff --git a/theater/frontline.py b/theater/frontline.py index c71ec4e3..c70b3417 100644 --- a/theater/frontline.py +++ b/theater/frontline.py @@ -4,11 +4,27 @@ from typing import Tuple from dcs.mapping import Point from . import ControlPoint, MissionTarget - # TODO: Dedup by moving everything to using this class. FRONTLINE_MIN_CP_DISTANCE = 5000 +def compute_position(control_point_a: ControlPoint, + control_point_b: ControlPoint) -> Point: + a = control_point_a.position + b = control_point_b.position + attack_heading = a.heading_between_point(b) + attack_distance = a.distance_to_point(b) + middle_point = a.point_from_heading(attack_heading, attack_distance / 2) + + strength_delta = float(control_point_a.base.strength - + control_point_b.base.strength) + position = middle_point.point_from_heading(attack_heading, + strength_delta * + attack_distance / 2 - + FRONTLINE_MIN_CP_DISTANCE) + return position + + class FrontLine(MissionTarget): """Defines a front line location between two control points. @@ -17,6 +33,8 @@ class FrontLine(MissionTarget): def __init__(self, control_point_a: ControlPoint, control_point_b: ControlPoint) -> None: + super().__init__(f"Front line {control_point_a}/{control_point_b}", + compute_position(control_point_a, control_point_b)) self.control_point_a = control_point_a self.control_point_b = control_point_b @@ -24,22 +42,3 @@ class FrontLine(MissionTarget): def control_points(self) -> Tuple[ControlPoint, ControlPoint]: """Returns a tuple of the two control points.""" return self.control_point_a, self.control_point_b - - @property - def name(self) -> str: - a = self.control_point_a.name - b = self.control_point_b.name - return f"Front line {a}/{b}" - - @property - def position(self) -> Point: - a = self.control_point_a.position - b = self.control_point_b.position - attack_heading = a.heading_between_point(b) - attack_distance = a.distance_to_point(b) - middle_point = a.point_from_heading(attack_heading, attack_distance / 2) - - strength_delta = (self.control_point_a.base.strength - self.control_point_b.base.strength) / 1.0 - position = middle_point.point_from_heading(attack_heading, - strength_delta * attack_distance / 2 - FRONTLINE_MIN_CP_DISTANCE) - return position diff --git a/theater/missiontarget.py b/theater/missiontarget.py index b0a30aa0..fb4da0f3 100644 --- a/theater/missiontarget.py +++ b/theater/missiontarget.py @@ -1,22 +1,18 @@ from __future__ import annotations -from abc import ABC, abstractmethod from dcs.mapping import Point -class MissionTarget(ABC): - # TODO: These should just be required objects to the constructor - # The TheatherGroundObject class is difficult to modify because it's - # generated data that's pickled ahead of time. - @property - @abstractmethod - def name(self) -> str: - """The name of the mission target.""" +class MissionTarget: + def __init__(self, name: str, position: Point) -> None: + """Initializes a mission target. - @property - @abstractmethod - def position(self) -> Point: - """The location of the mission target.""" + Args: + name: The name of the mission target. + position: The location of the mission target. + """ + self.name = name + self.position = position def distance_to(self, other: MissionTarget) -> int: """Computes the distance to the given mission target.""" diff --git a/theater/start_generator.py b/theater/start_generator.py index 135d50fc..9e679f6c 100644 --- a/theater/start_generator.py +++ b/theater/start_generator.py @@ -1,16 +1,19 @@ +from __future__ import annotations + import logging import math import pickle import random -import typing +from typing import Any, Dict, List, Optional from dcs.mapping import Point from dcs.task import CAP, CAS, PinpointStrike from dcs.vehicles import AirDefence -from game import db -from game.data.building_data import DEFAULT_AVAILABLE_BUILDINGS +from game import Game, db +from game.factions.faction import Faction from game.settings import Settings +from game.version import VERSION from gen import namegen from gen.defenses.armor_group_generator import generate_armor_group from gen.fleet.ship_group_generator import ( @@ -30,6 +33,13 @@ from theater import ( TheaterGroundObject, ) from theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW +from theater.theatergroundobject import ( + SamGroundObject, BuildingGroundObject, CarrierGroundObject, + LhaGroundObject, + MissileSiteGroundObject, ShipGroundObject, +) + +GroundObjectTemplates = Dict[str, Dict[str, Any]] UNIT_VARIETY = 6 UNIT_AMOUNT_FACTOR = 16 @@ -43,215 +53,387 @@ COUNT_BY_TASK = { } -def generate_initial_units(theater: ConflictTheater, enemy_country: str, sams: bool, multiplier: float): - for cp in theater.enemy_points(): - if cp.captured: - continue +class GameGenerator: + def __init__(self, player: str, enemy: str, theater: ConflictTheater, + settings: Settings, start_date, starting_budget: int, + multiplier: float, midgame: bool) -> None: + self.player = player + self.enemy = enemy + self.theater = theater + self.settings = settings + self.start_date = start_date + self.starting_budget = starting_budget + self.multiplier = multiplier + self.midgame = midgame + def generate(self) -> Game: + # Reset name generator + namegen.reset() + self.prepare_theater() + self.populate_red_airbases() + + game = Game(player_name=self.player, + enemy_name=self.enemy, + theater=self.theater, + start_date=self.start_date, + settings=self.settings) + + GroundObjectGenerator(game).generate() + game.budget = self.starting_budget + game.settings.multiplier = self.multiplier + game.settings.sams = True + game.settings.version = VERSION + return game + + def prepare_theater(self) -> None: + to_remove = [] + + # Auto-capture half the bases if midgame. + if self.midgame: + control_points = self.theater.controlpoints + for control_point in control_points[:len(control_points) // 2]: + control_point.captured = True + + # Remove carrier and lha, invert situation if needed + for cp in self.theater.controlpoints: + no_carrier = self.settings.do_not_generate_carrier + no_lha = self.settings.do_not_generate_lha + if cp.cptype is ControlPointType.AIRCRAFT_CARRIER_GROUP and \ + no_carrier: + to_remove.append(cp) + elif cp.cptype is ControlPointType.LHA_GROUP and no_lha: + to_remove.append(cp) + + if self.settings.inverted: + cp.captured = cp.captured_invert + + # do remove + for cp in to_remove: + self.theater.controlpoints.remove(cp) + + # TODO: Fix this. This captures all bases for blue. + # reapply midgame inverted if needed + if self.midgame and self.settings.inverted: + for i, cp in enumerate(reversed(self.theater.controlpoints)): + if i > len(self.theater.controlpoints): + break + else: + cp.captured = True + + def populate_red_airbases(self) -> None: + for control_point in self.theater.enemy_points(): + if control_point.captured: + continue + self.populate_red_airbase(control_point) + + def populate_red_airbase(self, control_point: ControlPoint) -> None: # Force reset cp on generation - cp.base.aircraft = {} - cp.base.armor = {} - cp.base.aa = {} - cp.base.commision_points = {} - cp.base.strength = 1 + control_point.base.aircraft = {} + control_point.base.armor = {} + control_point.base.aa = {} + control_point.base.commision_points = {} + control_point.base.strength = 1 for task in [PinpointStrike, CAP, CAS, AirDefence]: - assert cp.importance <= IMPORTANCE_HIGH, "invalid importance {}".format(cp.importance) - assert cp.importance >= IMPORTANCE_LOW, "invalid importance {}".format(cp.importance) + if IMPORTANCE_HIGH <= control_point.importance <= IMPORTANCE_LOW: + raise ValueError( + f"CP importance must be between {IMPORTANCE_LOW} and " + f"{IMPORTANCE_HIGH}, is {control_point.importance}") - importance_factor = (cp.importance - IMPORTANCE_LOW) / (IMPORTANCE_HIGH - IMPORTANCE_LOW) - variety = int(UNIT_VARIETY) - unittypes = db.choose_units(task, importance_factor, variety, enemy_country) - - if not sams and task == AirDefence: - unittypes = [x for x in db.find_unittype(AirDefence, enemy_country) if x not in db.SAM_BAN] - - count_log = math.log(cp.importance + 0.01, UNIT_COUNT_IMPORTANCE_LOG) - count = max(COUNT_BY_TASK[task] * multiplier * (1+count_log), 1) - - if len(unittypes) > 0: - count_per_type = max(int(float(count) / len(unittypes)), 1) - for unit_type in unittypes: - logging.info("{} - {} {}".format(cp.name, db.unit_type_name(unit_type), count_per_type)) - cp.base.commision_units({unit_type: count_per_type}) - - -def generate_groundobjects(theater: ConflictTheater, game): - with open("resources/groundobject_templates.p", "rb") as f: - tpls = pickle.load(f) - - group_id = 0 - cp_to_remove = [] - for cp in theater.controlpoints: - group_id = generate_cp_ground_points(cp, theater, game, group_id, tpls) - - # CP - if cp.captured: - faction_name = game.player_name - else: - faction_name = game.enemy_name - faction = db.FACTIONS[faction_name] - - if cp.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP: - # Create ground object group - group_id = game.next_group_id() - g = TheaterGroundObject("CARRIER") - g.group_id = group_id - g.object_id = 0 - g.cp_id = cp.id - g.airbase_group = True - g.dcs_identifier = "CARRIER" - g.sea_object = True - g.obj_name = namegen.random_objective_name() - g.heading = 0 - g.position = Point(cp.position.x, cp.position.y) - group = generate_carrier_group(faction_name, game, g) - g.groups = [] - if group is not None: - g.groups.append(group) - cp.ground_objects.append(g) - # Set new name : - if len(faction.carrier_names) > 0: - cp.name = random.choice(faction.carrier_names) - else: - cp_to_remove.append(cp) - elif cp.cptype == ControlPointType.LHA_GROUP: - # Create ground object group - group_id = game.next_group_id() - g = TheaterGroundObject("LHA") - g.group_id = group_id - g.object_id = 0 - g.cp_id = cp.id - g.airbase_group = True - g.dcs_identifier = "LHA" - g.sea_object = True - g.obj_name = namegen.random_objective_name() - g.heading = 0 - g.position = Point(cp.position.x, cp.position.y) - group = generate_lha_group(faction_name, game, g) - g.groups = [] - if group is not None: - g.groups.append(group) - cp.ground_objects.append(g) - # Set new name : - if len(faction.helicopter_carrier_names) > 0: - cp.name = random.choice(faction.helicopter_carrier_names) - else: - cp_to_remove.append(cp) - else: - - for i in range(random.randint(3, 6)): - - logging.info("GENERATE BASE DEFENSE") - point = find_location(True, cp.position, theater, 800, 3200, [], True) - logging.info(point) - - if point is None: - logging.info("Couldn't find point for {} base defense".format(cp)) - continue - - group_id = game.next_group_id() - - g = TheaterGroundObject("aa") - g.group_id = group_id - g.object_id = 0 - g.cp_id = cp.id - g.airbase_group = True - g.dcs_identifier = "AA" - g.sea_object = False - g.obj_name = namegen.random_objective_name() - g.heading = 0 - g.position = Point(point.x, point.y) - - generate_airbase_defense_group(i, g, faction_name, game, cp) - cp.ground_objects.append(g) - - logging.info("---------------------------") - logging.info("CP Generation : " + cp.name) - for ground_object in cp.ground_objects: - logging.info(ground_object.groups) - - # Generate navy groups - if len(faction.navy_generators) > 0 and cp.allow_sea_units: - - if cp.captured and game.settings.do_not_generate_player_navy: - continue - if not cp.captured and game.settings.do_not_generate_enemy_navy: + importance_factor = ((control_point.importance - IMPORTANCE_LOW) / + (IMPORTANCE_HIGH - IMPORTANCE_LOW)) + # noinspection PyTypeChecker + unit_types = db.choose_units(task, importance_factor, UNIT_VARIETY, + self.enemy) + if not unit_types: continue - for i in range(faction.navy_group_count): + count_log = math.log(control_point.importance + 0.01, + UNIT_COUNT_IMPORTANCE_LOG) + count = max( + COUNT_BY_TASK[task] * self.multiplier * (1 + count_log), 1 + ) - point = find_location(False, cp.position, theater, 5000, 40000, [], False) - - if point is None: - logging.info("Couldn't find point for {} ships".format(cp)) - continue - - group_id = game.next_group_id() - - g = TheaterGroundObject("aa") - g.group_id = group_id - g.object_id = 0 - g.cp_id = cp.id - g.airbase_group = False - g.dcs_identifier = "AA" - g.sea_object = True - g.obj_name = namegen.random_objective_name() - g.heading = 0 - g.position = Point(point.x, point.y) - - group = generate_ship_group(game, g, faction_name) - g.groups = [] - if group is not None: - g.groups.append(group) - cp.ground_objects.append(g) - - if len(faction.missiles) > 0: - - for i in range(faction.missiles_group_count): - - point = find_location(True, cp.position, theater, 2500, 40000, [], False) - - if point is None: - logging.info("Couldn't find point for {} missiles".format(cp)) - continue - - group_id = game.next_group_id() - - g = TheaterGroundObject("aa") - g.group_id = group_id - g.object_id = 0 - g.cp_id = cp.id - g.airbase_group = False - g.dcs_identifier = "AA" - g.sea_object = False - g.obj_name = namegen.random_objective_name() - g.heading = 0 - g.position = Point(point.x, point.y) - - group = generate_missile_group(game, g, faction_name) - g.groups = [] - if group is not None: - g.groups.append(group) - cp.ground_objects.append(g) - - for cp in cp_to_remove: - theater.controlpoints.remove(cp) + count_per_type = max(int(float(count) / len(unit_types)), 1) + for unit_type in unit_types: + control_point.base.commision_units({unit_type: count_per_type}) +class ControlPointGroundObjectGenerator: + def __init__(self, game: Game, control_point: ControlPoint, + templates: GroundObjectTemplates) -> None: + self.game = game + self.control_point = control_point + self.templates = templates -def generate_airbase_defense_group(airbase_defense_group_id, ground_obj:TheaterGroundObject, faction, game, cp): + @property + def faction_name(self) -> str: + if self.control_point.captured: + return self.game.player_name + else: + return self.game.enemy_name - logging.info("GENERATE AIR DEFENSE GROUP") - logging.info(faction) - logging.info(airbase_defense_group_id) + @property + def faction(self) -> Faction: + return db.FACTIONS[self.faction_name] + def generate(self) -> bool: + self.control_point.ground_objects = [] + self.generate_ground_points() + if self.faction.navy_generators: + # Even airbases can generate navies if they are close enough to the + # water. This is not controlled by the control point definition, but + # rather by whether or not the generator can find a valid position + # for the ship. + self.generate_navy() + + if self.faction.missiles: + # TODO: Presumably only for airbases? + self.generate_missile_sites() + + return True + + def generate_ground_points(self) -> None: + """Generate ground objects and AA sites for the control point.""" + + if self.control_point.is_global: + return + + # TODO: Should probably perform this check later. + # Just because we don't have factories for the faction doesn't mean we + # shouldn't generate AA. + available_categories = self.faction.building_set + if not available_categories: + return + + # Always generate at least one AA point. + self.generate_aa_site() + + # And between 2 and 7 other objectives. + amount = random.randrange(2, 7) + for i in range(amount): + # 1 in 4 additional objectives are AA. + if random.randint(0, 3) == 0: + self.generate_aa_site() + else: + category = random.choice(available_categories) + self.generate_ground_point(category) + + def generate_ground_point(self, category: str) -> None: + obj_name = namegen.random_objective_name() + template = random.choice(list(self.templates[category].values())) + point = find_location(category != "oil", + self.control_point.position, + self.game.theater, 10000, 40000, + self.control_point.ground_objects) + + if point is None: + logging.error( + f"Could not find point for {obj_name} at {self.control_point}") + return + + object_id = 0 + group_id = self.game.next_group_id() + + # TODO: Create only one TGO per objective, each with multiple units. + for unit in template: + object_id += 1 + + template_point = Point(unit["offset"].x, unit["offset"].y) + g = BuildingGroundObject( + obj_name, category, group_id, object_id, point + template_point, + unit["heading"], self.control_point, unit["type"]) + + self.control_point.ground_objects.append(g) + + def generate_aa_site(self) -> None: + obj_name = namegen.random_objective_name() + position = find_location(True, self.control_point.position, + self.game.theater, 10000, 40000, + self.control_point.ground_objects) + + if position is None: + logging.error( + f"Could not find point for {obj_name} at {self.control_point}") + return + + group_id = self.game.next_group_id() + + g = SamGroundObject(namegen.random_objective_name(), group_id, + position, self.control_point, for_airbase=False) + group = generate_anti_air_group(self.game, g, self.faction_name) + if group is not None: + g.groups = [group] + self.control_point.ground_objects.append(g) + + def generate_navy(self) -> None: + skip_player_navy = self.game.settings.do_not_generate_player_navy + if self.control_point.captured and skip_player_navy: + return + + skip_enemy_navy = self.game.settings.do_not_generate_enemy_navy + if not self.control_point.captured and skip_enemy_navy: + return + + for _ in range(self.faction.navy_group_count): + self.generate_ship() + + def generate_ship(self) -> None: + point = find_location(False, self.control_point.position, + self.game.theater, 5000, 40000, [], False) + if point is None: + logging.error( + f"Could not find point for {self.control_point}'s navy") + return + + group_id = self.game.next_group_id() + + g = ShipGroundObject(namegen.random_objective_name(), group_id, point, + self.control_point) + + group = generate_ship_group(self.game, g, self.faction_name) + g.groups = [] + if group is not None: + g.groups.append(group) + self.control_point.ground_objects.append(g) + + def generate_missile_sites(self) -> None: + for i in range(self.faction.missiles_group_count): + self.generate_missile_site() + + def generate_missile_site(self) -> None: + point = find_location(True, self.control_point.position, + self.game.theater, 2500, 40000, [], False) + if point is None: + logging.info( + f"Could not find point for {self.control_point} missile site") + return + + group_id = self.game.next_group_id() + + g = MissileSiteGroundObject(namegen.random_objective_name(), group_id, + point, self.control_point) + group = generate_missile_group(self.game, g, self.faction_name) + g.groups = [] + if group is not None: + g.groups.append(group) + self.control_point.ground_objects.append(g) + return + + +class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator): + def generate(self) -> bool: + if not super().generate(): + return False + + carrier_names = self.faction.carrier_names + if not carrier_names: + logging.info( + f"Skipping generation of {self.control_point.name} because " + f"{self.faction_name} has no carriers") + return False + + # Create ground object group + group_id = self.game.next_group_id() + g = CarrierGroundObject(namegen.random_objective_name(), group_id, + self.control_point) + group = generate_carrier_group(self.faction_name, self.game, g) + g.groups = [] + if group is not None: + g.groups.append(group) + self.control_point.ground_objects.append(g) + self.control_point.name = random.choice(carrier_names) + return True + + +class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator): + def generate(self) -> bool: + if not super().generate(): + return False + + lha_names = self.faction.helicopter_carrier_names + if not lha_names: + logging.info( + f"Skipping generation of {self.control_point.name} because " + f"{self.faction_name} has no LHAs") + return False + + # Create ground object group + group_id = self.game.next_group_id() + g = LhaGroundObject(namegen.random_objective_name(), group_id, + self.control_point) + group = generate_lha_group(self.faction_name, self.game, g) + g.groups = [] + if group is not None: + g.groups.append(group) + self.control_point.ground_objects.append(g) + self.control_point.name = random.choice(lha_names) + return True + + +class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): + def generate(self) -> bool: + if not super().generate(): + return False + + for i in range(random.randint(3, 6)): + self.generate_sam(i) + return True + + def generate_sam(self, index: int) -> None: + position = find_location(True, self.control_point.position, + self.game.theater, 800, 3200, [], True) + if position is None: + logging.error("Could not find position for " + f"{self.control_point} base defense") + return + + group_id = self.game.next_group_id() + + g = SamGroundObject(namegen.random_objective_name(), group_id, + position, self.control_point, for_airbase=True) + + generate_airbase_defense_group(index, g, self.faction_name, self.game) + self.control_point.ground_objects.append(g) + + +class GroundObjectGenerator: + def __init__(self, game: Game) -> None: + self.game = game + with open("resources/groundobject_templates.p", "rb") as f: + self.templates: GroundObjectTemplates = pickle.load(f) + + def generate(self) -> None: + # Copied so we can remove items from the original list without breaking + # the iterator. + control_points = list(self.game.theater.controlpoints) + for control_point in control_points: + if not self.generate_for_control_point(control_point): + self.game.theater.controlpoints.remove(control_point) + + def generate_for_control_point(self, control_point: ControlPoint) -> bool: + generator: ControlPointGroundObjectGenerator + if control_point.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP: + generator = CarrierGroundObjectGenerator(self.game, control_point, + self.templates) + elif control_point.cptype == ControlPointType.LHA_GROUP: + generator = LhaGroundObjectGenerator(self.game, control_point, + self.templates) + else: + generator = AirbaseGroundObjectGenerator(self.game, control_point, + self.templates) + return generator.generate() + + +def generate_airbase_defense_group(airbase_defense_group_id: int, + ground_obj: TheaterGroundObject, + faction: str, game: Game) -> None: if airbase_defense_group_id == 0: group = generate_armor_group(faction, game, ground_obj) elif airbase_defense_group_id == 1 and random.randint(0, 1) == 0: - group = generate_anti_air_group(game, cp, ground_obj, faction) + group = generate_anti_air_group(game, ground_obj, faction) elif random.randint(0, 2) == 1: - group = generate_shorad_group(game, cp, ground_obj, faction) + group = generate_shorad_group(game, ground_obj, faction) else: group = generate_armor_group(faction, game, ground_obj) @@ -260,22 +442,31 @@ def generate_airbase_defense_group(airbase_defense_group_id, ground_obj:TheaterG ground_obj.groups.append(group) -def find_location(on_ground, near, theater, min, max, others, is_base_defense=False) -> typing.Optional[Point]: +# TODO: https://stackoverflow.com/a/19482012/632035 +# A lot of the time spent on mission generation is spent in this function since +# just randomly guess up to 1800 times and often fail. This is particularly +# problematic while trying to find placement for navies in Nevada. +def find_location(on_ground: bool, near: Point, theater: ConflictTheater, + min_range: int, max_range: int, + others: List[TheaterGroundObject], + is_base_defense: bool = False) -> Optional[Point]: """ Find a valid ground object location - :param on_ground: Whether it should be on ground or on sea (True = on ground) + :param on_ground: Whether it should be on ground or on sea (True = on + ground) :param near: Point :param theater: Theater object - :param min: Minimal range from point - :param max: Max range from point + :param min_range: Minimal range from point + :param max_range: Max range from point :param others: Other already existing ground objects + :param is_base_defense: True if the location is for base defense. :return: """ point = None for _ in range(300): # Check if on land or sea - p = near.random_point_within(max, min) + p = near.random_point_within(max_range, min_range) if on_ground and theater.is_on_land(p): point = p elif not on_ground and theater.is_in_sea(p): @@ -297,15 +488,16 @@ def find_location(on_ground, near, theater, min, max, others, is_base_defense=Fa break if point: - for other in theater.controlpoints: - if is_base_defense: break - if other.position != near: + for control_point in theater.controlpoints: + if is_base_defense: + break + if control_point.position != near: if point is None: break - if other.position.distance_to_point(point) < 30000: + if control_point.position.distance_to_point(point) < 30000: point = None break - for ground_obj in other.ground_objects: + for ground_obj in control_point.ground_objects: if ground_obj.position.distance_to_point(point) < 10000: point = None break @@ -313,114 +505,3 @@ def find_location(on_ground, near, theater, min, max, others, is_base_defense=Fa if point: return point return None - - -def generate_cp_ground_points(cp: ControlPoint, theater, game, group_id, templates): - """ - Generate inital ground objects and AA site for given control point - :param cp: Control point to initialize - :param theater: Theater - :param game: Game object - :param group_id: Group id - :param templates: Ground object templates - :return: True if something was generated - """ - # Reset cp ground objects - cp.ground_objects = [] - - if cp.is_global: - return False - - if cp.captured: - faction = game.player_name - else: - faction = game.enemy_name - faction_data = db.FACTIONS[faction] - - available_categories = faction_data.building_set - - if len(available_categories) == 0: - return False - - amount = random.randrange(3, 8) - for i in range(0, amount): - - obj_name = namegen.random_objective_name() - - if i >= amount - 1: - tpl_category = "aa" - else: - if random.randint(0, 3) == 0: - tpl_category = "aa" - else: - tpl_category = random.choice(available_categories) - - tpl = random.choice(list(templates[tpl_category].values())) - point = find_location(tpl_category != "oil", cp.position, theater, 10000, 40000, cp.ground_objects) - - if point is None: - logging.info("Couldn't find point for {}".format(cp)) - continue - - object_id = 0 - group_id = game.next_group_id() - - logging.info("generated {} for {}".format(tpl_category, cp)) - - for object in tpl: - object_id += 1 - - g = TheaterGroundObject(tpl_category) - g.group_id = group_id - g.object_id = object_id - g.cp_id = cp.id - g.airbase_group = False - g.obj_name = obj_name - - g.dcs_identifier = object["type"] - g.heading = object["heading"] - g.sea_object = False - g.position = Point(point.x + object["offset"].x, point.y + object["offset"].y) - - if g.dcs_identifier == "AA": - g.groups = [] - group = generate_anti_air_group(game, cp, g, faction) - if group is not None: - g.groups.append(group) - - cp.ground_objects.append(g) - return group_id - - -def prepare_theater(theater: ConflictTheater, settings:Settings, midgame): - - to_remove = [] - - # autocapture half the base if midgame - if midgame: - for i in range(0, int(len(theater.controlpoints) / 2)): - theater.controlpoints[i].captured = True - - # Remove carrier and lha, invert situation if needed - for cp in theater.controlpoints: - if cp.cptype is ControlPointType.AIRCRAFT_CARRIER_GROUP and settings.do_not_generate_carrier: - to_remove.append(cp) - elif cp.cptype is ControlPointType.LHA_GROUP and settings.do_not_generate_lha: - to_remove.append(cp) - - if settings.inverted: - cp.captured = cp.captured_invert - - # do remove - for cp in to_remove: - theater.controlpoints.remove(cp) - - # reapply midgame inverted if needed - if midgame and settings.inverted: - for i, cp in enumerate(reversed(theater.controlpoints)): - if i > len(theater.controlpoints): - break - else: - cp.captured = True - - diff --git a/theater/theatergroundobject.py b/theater/theatergroundobject.py index 659e366e..e3e97f38 100644 --- a/theater/theatergroundobject.py +++ b/theater/theatergroundobject.py @@ -1,4 +1,5 @@ -import uuid +from __future__ import annotations + from typing import List, TYPE_CHECKING from dcs.mapping import Point @@ -68,21 +69,21 @@ CATEGORY_MAP = { class TheaterGroundObject(MissionTarget): - cp_id = 0 - group_id = 0 - object_id = 0 - dcs_identifier = None # type: str - is_dead = False - airbase_group = False - heading = 0 - position = None # type: Point - groups: List[Group] = [] - obj_name = "" - sea_object = False - uuid = uuid.uuid1() - def __init__(self, category: str): + def __init__(self, name: str, category: str, group_id: int, object_id: int, + position: Point, heading: int, cp_id: int, dcs_identifier: str, + airbase_group: bool, sea_object: bool) -> None: + super().__init__(name, position) self.category = category + self.group_id = group_id + self.object_id = object_id + self.heading = heading + self.cp_id = cp_id + self.dcs_identifier = dcs_identifier + self.airbase_group = airbase_group + self.sea_object = sea_object + self.is_dead = False + self.groups: List[Group] = [] @property def string_identifier(self): @@ -96,20 +97,128 @@ class TheaterGroundObject(MissionTarget): def name_abbrev(self) -> str: return ABBREV_NAME[self.category] - def __str__(self): + def __str__(self) -> str: return NAME_BY_CATEGORY[self.category] - def matches_string_identifier(self, id): - return self.string_identifier == id + def matches_string_identifier(self, identifier): + return self.string_identifier == identifier @property - def name(self) -> str: - return self.obj_name + def obj_name(self) -> str: + return self.name - def parent_control_point( - self, theater: "ConflictTheater") -> "ControlPoint": + def parent_control_point(self, theater: ConflictTheater) -> ControlPoint: """Searches the theater for the parent control point.""" for cp in theater.controlpoints: if cp.id == self.cp_id: return cp raise RuntimeError("Could not find matching control point in theater") + + +class BuildingGroundObject(TheaterGroundObject): + def __init__(self, name: str, category: str, group_id: int, object_id: int, + position: Point, heading: int, control_point: ControlPoint, + dcs_identifier: str) -> None: + super().__init__( + name=name, + category=category, + group_id=group_id, + object_id=object_id, + position=position, + heading=heading, + cp_id=control_point.id, + dcs_identifier=dcs_identifier, + airbase_group=False, + sea_object=False + ) + + +class GenericCarrierGroundObject(TheaterGroundObject): + pass + + +# TODO: Why is this both a CP and a TGO? +class CarrierGroundObject(GenericCarrierGroundObject): + def __init__(self, name: str, group_id: int, + control_point: ControlPoint) -> None: + super().__init__( + name=name, + category="CARRIER", + group_id=group_id, + object_id=0, + position=control_point.position, + heading=0, + cp_id=control_point.id, + dcs_identifier="CARRIER", + airbase_group=True, + sea_object=True + ) + + +# TODO: Why is this both a CP and a TGO? +class LhaGroundObject(GenericCarrierGroundObject): + def __init__(self, name: str, group_id: int, + control_point: ControlPoint) -> None: + super().__init__( + name=name, + category="LHA", + group_id=group_id, + object_id=0, + position=control_point.position, + heading=0, + cp_id=control_point.id, + dcs_identifier="LHA", + airbase_group=True, + sea_object=True + ) + + +class MissileSiteGroundObject(TheaterGroundObject): + def __init__(self, name: str, group_id: int, position: Point, + control_point: ControlPoint) -> None: + super().__init__( + name=name, + category="aa", + group_id=group_id, + object_id=0, + position=position, + heading=0, + cp_id=control_point.id, + dcs_identifier="AA", + airbase_group=False, + sea_object=False + ) + + +class SamGroundObject(TheaterGroundObject): + def __init__(self, name: str, group_id: int, position: Point, + control_point: ControlPoint, for_airbase: bool) -> None: + super().__init__( + name=name, + category="aa", + group_id=group_id, + object_id=0, + position=position, + heading=0, + cp_id=control_point.id, + dcs_identifier="AA", + airbase_group=for_airbase, + sea_object=False + ) + + +class ShipGroundObject(TheaterGroundObject): + def __init__(self, name: str, group_id: int, position: Point, + control_point: ControlPoint) -> None: + super().__init__( + name=name, + category="aa", + group_id=group_id, + object_id=0, + position=position, + heading=0, + cp_id=control_point.id, + dcs_identifier="AA", + airbase_group=False, + sea_object=True + )