mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Unify TGO location selection.
We currently have three methods of choosing locations for TGOs: 1. From the campaign miz 2. From the per-CP mizdata files 3. Randomly Move the selection among these sources into a single place and use it everywhere that we search for a TGO location. Longer term methods 2 and 3 will be removed.
This commit is contained in:
parent
df80ec635f
commit
1ee0aafd9a
@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
@ -38,6 +39,19 @@ class ControlPointType(Enum):
|
||||
FOB = 5 # A FOB (ground units only)
|
||||
|
||||
|
||||
class LocationType(Enum):
|
||||
BaseAirDefense = "base air defense"
|
||||
Coastal = "coastal defense"
|
||||
Ewr = "EWR"
|
||||
Garrison = "garrison"
|
||||
MissileSite = "missile site"
|
||||
OffshoreStrikeTarget = "offshore strike target"
|
||||
Sam = "SAM"
|
||||
Ship = "ship"
|
||||
Shorad = "SHORAD"
|
||||
StrikeTarget = "strike target"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PresetLocations:
|
||||
base_garrisons: List[Point] = field(default_factory=list)
|
||||
@ -45,9 +59,12 @@ class PresetLocations:
|
||||
|
||||
ewrs: List[Point] = field(default_factory=list)
|
||||
sams: List[Point] = field(default_factory=list)
|
||||
offshore: List[Point] = field(default_factory=list)
|
||||
coastal_defenses: List[Point] = field(default_factory=list)
|
||||
strike_locations: List[Point] = field(default_factory=list)
|
||||
|
||||
fixed_sams: List[Point] = field(default_factory=list)
|
||||
|
||||
@staticmethod
|
||||
def _random_from(points: List[Point]) -> Optional[Point]:
|
||||
if not points:
|
||||
@ -56,23 +73,25 @@ class PresetLocations:
|
||||
points.remove(point)
|
||||
return point
|
||||
|
||||
def random_garrison(self) -> Optional[Point]:
|
||||
return self._random_from(self.base_garrisons)
|
||||
|
||||
def random_base_sam(self) -> Optional[Point]:
|
||||
return self._random_from(self.base_air_defense)
|
||||
|
||||
def random_ewr(self) -> Optional[Point]:
|
||||
return self._random_from(self.ewrs)
|
||||
|
||||
def random_sam(self) -> Optional[Point]:
|
||||
return self._random_from(self.sams)
|
||||
|
||||
def random_coastal_defense(self) -> Optional[Point]:
|
||||
return self._random_from(self.coastal_defenses)
|
||||
|
||||
def random_strike_location(self) -> Optional[Point]:
|
||||
return self._random_from(self.strike_locations)
|
||||
def random_for(self, location_type: LocationType) -> Optional[Point]:
|
||||
if location_type == LocationType.Garrison:
|
||||
return self._random_from(self.base_garrisons)
|
||||
if location_type == LocationType.Sam:
|
||||
return self._random_from(self.sams)
|
||||
if location_type == LocationType.BaseAirDefense:
|
||||
return self._random_from(self.base_air_defense)
|
||||
if location_type == LocationType.Ewr:
|
||||
return self._random_from(self.ewrs)
|
||||
if location_type == LocationType.Shorad:
|
||||
return self._random_from(self.base_garrisons)
|
||||
if location_type == LocationType.OffshoreStrikeTarget:
|
||||
return self._random_from(self.offshore)
|
||||
if location_type == LocationType.Ship:
|
||||
return self._random_from(self.offshore)
|
||||
if location_type == LocationType.StrikeTarget:
|
||||
return self._random_from(self.strike_locations)
|
||||
logging.error(f"Unknown location type: {location_type}")
|
||||
return None
|
||||
|
||||
|
||||
class ControlPoint(MissionTarget):
|
||||
|
||||
@ -4,7 +4,7 @@ import logging
|
||||
import math
|
||||
import pickle
|
||||
import random
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import CAP, CAS, PinpointStrike
|
||||
@ -13,6 +13,7 @@ from dcs.vehicles import AirDefence
|
||||
from game import Game, db
|
||||
from game.factions.faction import Faction
|
||||
from game.settings import Settings
|
||||
from game.theater import LocationType
|
||||
from game.theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
@ -32,8 +33,7 @@ from gen.fleet.ship_group_generator import (
|
||||
generate_lha_group,
|
||||
generate_ship_group,
|
||||
)
|
||||
from gen.locations.preset_location_finder import PresetLocationFinder
|
||||
from gen.locations.preset_locations import PresetLocation
|
||||
from gen.locations.preset_location_finder import MizDataLocationFinder
|
||||
from gen.missiles.missiles_group_generator import generate_missile_group
|
||||
from gen.sam.sam_group_generator import (
|
||||
generate_anti_air_group,
|
||||
@ -164,11 +164,155 @@ class GameGenerator:
|
||||
control_point.base.commision_units({unit_type: count_per_type})
|
||||
|
||||
|
||||
class LocationFinder:
|
||||
def __init__(self, game: Game, control_point: ControlPoint) -> None:
|
||||
self.game = game
|
||||
self.control_point = control_point
|
||||
self.miz_data = MizDataLocationFinder.compute_possible_locations(
|
||||
game.theater.terrain.name, control_point.full_name)
|
||||
|
||||
def location_for(self, location_type: LocationType) -> Optional[Point]:
|
||||
position = self.control_point.preset_locations.random_for(location_type)
|
||||
if position is not None:
|
||||
return position
|
||||
|
||||
logging.warning(f"No campaign location for %s at %s",
|
||||
location_type.value, self.control_point)
|
||||
position = self.random_from_miz_data(
|
||||
location_type == LocationType.OffshoreStrikeTarget)
|
||||
if position is not None:
|
||||
return position
|
||||
|
||||
logging.debug(f"No mizdata location for %s at %s", location_type.value,
|
||||
self.control_point)
|
||||
position = self.random_position(location_type)
|
||||
if position is not None:
|
||||
return position
|
||||
|
||||
logging.error(f"Could not find position for %s at %s",
|
||||
location_type.value, self.control_point)
|
||||
return None
|
||||
|
||||
def random_from_miz_data(self, offshore: bool) -> Optional[Point]:
|
||||
if offshore:
|
||||
locations = self.miz_data.offshore_locations
|
||||
else:
|
||||
locations = self.miz_data.ashore_locations
|
||||
if self.miz_data.offshore_locations:
|
||||
preset = random.choice(locations)
|
||||
locations.remove(preset)
|
||||
return preset.position
|
||||
return None
|
||||
|
||||
def random_position(self, location_type: LocationType) -> Optional[Point]:
|
||||
# TODO: Flesh out preset locations so we never hit this case.
|
||||
logging.warning("Falling back to random location for %s at %s",
|
||||
location_type.value, self.control_point)
|
||||
|
||||
is_base_defense = location_type in {
|
||||
LocationType.BaseAirDefense,
|
||||
LocationType.Garrison,
|
||||
LocationType.Shorad,
|
||||
}
|
||||
|
||||
on_land = location_type not in {
|
||||
LocationType.OffshoreStrikeTarget,
|
||||
LocationType.Ship,
|
||||
}
|
||||
|
||||
avoid_others = location_type not in {
|
||||
LocationType.Garrison,
|
||||
LocationType.MissileSite,
|
||||
LocationType.Sam,
|
||||
LocationType.Ship,
|
||||
LocationType.Shorad,
|
||||
}
|
||||
|
||||
if is_base_defense:
|
||||
min_range = 400
|
||||
max_range = 3200
|
||||
elif location_type == LocationType.Ship:
|
||||
min_range = 5000
|
||||
max_range = 40000
|
||||
elif location_type == LocationType.MissileSite:
|
||||
min_range = 2500
|
||||
max_range = 40000
|
||||
else:
|
||||
min_range = 10000
|
||||
max_range = 40000
|
||||
|
||||
position = self._find_random_position(min_range, max_range,
|
||||
on_land, is_base_defense,
|
||||
avoid_others)
|
||||
|
||||
# Retry once, searching a bit further (On some big airbases, 3200 is too
|
||||
# short (Ex : Incirlik)), but searching farther on every base would be
|
||||
# problematic, as some base defense units would end up very far away
|
||||
# from small airfields.
|
||||
if position is None and is_base_defense:
|
||||
position = self._find_random_position(3200, 4800,
|
||||
on_land, is_base_defense,
|
||||
avoid_others)
|
||||
return position
|
||||
|
||||
def _find_random_position(self, min_range: int, max_range: int,
|
||||
on_ground: bool, is_base_defense: bool,
|
||||
avoid_others: bool) -> Optional[Point]:
|
||||
"""
|
||||
Find a valid ground object location
|
||||
:param on_ground: Whether it should be on ground or on sea (True = on
|
||||
ground)
|
||||
:param theater: Theater object
|
||||
:param min_range: Minimal range from point
|
||||
:param max_range: Max range from point
|
||||
:param is_base_defense: True if the location is for base defense.
|
||||
:return:
|
||||
"""
|
||||
near = self.control_point.position
|
||||
others = self.control_point.ground_objects
|
||||
|
||||
def is_valid(point: Optional[Point]) -> bool:
|
||||
if point is None:
|
||||
return False
|
||||
|
||||
if on_ground and not self.game.theater.is_on_land(point):
|
||||
return False
|
||||
elif not on_ground and not self.game.theater.is_in_sea(point):
|
||||
return False
|
||||
|
||||
if avoid_others:
|
||||
for other in others:
|
||||
if other.position.distance_to_point(point) < 10000:
|
||||
return False
|
||||
|
||||
if is_base_defense:
|
||||
# If it's a base defense we don't care how close it is to other
|
||||
# points.
|
||||
return True
|
||||
|
||||
# Else verify that it's not too close to another control point.
|
||||
for control_point in self.game.theater.controlpoints:
|
||||
if control_point != self.control_point:
|
||||
if control_point.position.distance_to_point(point) < 30000:
|
||||
return False
|
||||
for ground_obj in control_point.ground_objects:
|
||||
if ground_obj.position.distance_to_point(point) < 10000:
|
||||
return False
|
||||
return True
|
||||
|
||||
for _ in range(300):
|
||||
# Check if on land or sea
|
||||
p = near.random_point_within(max_range, min_range)
|
||||
if is_valid(p):
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
class ControlPointGroundObjectGenerator:
|
||||
def __init__(self, game: Game, control_point: ControlPoint) -> None:
|
||||
self.game = game
|
||||
self.control_point = control_point
|
||||
self.preset_locations = PresetLocationFinder.compute_possible_locations(game.theater.terrain.name, control_point.full_name)
|
||||
self.location_finder = LocationFinder(game, control_point)
|
||||
|
||||
@property
|
||||
def faction_name(self) -> str:
|
||||
@ -205,11 +349,9 @@ class ControlPointGroundObjectGenerator:
|
||||
self.generate_ship()
|
||||
|
||||
def generate_ship(self) -> None:
|
||||
point = find_location(False, self.control_point.position,
|
||||
self.game.theater, 5000, 40000, [], False)
|
||||
point = self.location_finder.location_for(
|
||||
LocationType.OffshoreStrikeTarget)
|
||||
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()
|
||||
@ -223,27 +365,6 @@ class ControlPointGroundObjectGenerator:
|
||||
g.groups.append(group)
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def pick_preset_location(self, offshore=False) -> Optional[PresetLocation]:
|
||||
"""
|
||||
Return a preset location if any is setup and still available for this point
|
||||
@:param offshore Whether this should be an offshore location
|
||||
@:return The preset location if found; None if it couldn't be found
|
||||
"""
|
||||
if offshore:
|
||||
if len(self.preset_locations.offshore_locations) > 0:
|
||||
location = random.choice(self.preset_locations.offshore_locations)
|
||||
self.preset_locations.offshore_locations.remove(location)
|
||||
logging.info("Picked a preset offshore location")
|
||||
return location
|
||||
else:
|
||||
if len(self.preset_locations.ashore_locations) > 0:
|
||||
location = random.choice(self.preset_locations.ashore_locations)
|
||||
self.preset_locations.ashore_locations.remove(location)
|
||||
logging.info("Picked a preset ashore location")
|
||||
return location
|
||||
logging.info("No preset location found")
|
||||
return None
|
||||
|
||||
|
||||
class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
def generate(self) -> bool:
|
||||
@ -299,6 +420,7 @@ class BaseDefenseGenerator:
|
||||
def __init__(self, game: Game, control_point: ControlPoint) -> None:
|
||||
self.game = game
|
||||
self.control_point = control_point
|
||||
self.location_finder = LocationFinder(game, control_point)
|
||||
|
||||
@property
|
||||
def faction_name(self) -> str:
|
||||
@ -317,8 +439,7 @@ class BaseDefenseGenerator:
|
||||
self.generate_base_defenses()
|
||||
|
||||
def generate_ewr(self) -> None:
|
||||
position = self._find_location(
|
||||
"EWR", self.control_point.preset_locations.random_ewr)
|
||||
position = self.location_finder.location_for(LocationType.Ewr)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
@ -349,8 +470,7 @@ class BaseDefenseGenerator:
|
||||
self.generate_garrison()
|
||||
|
||||
def generate_garrison(self) -> None:
|
||||
position = self._find_location(
|
||||
"garrison", self.control_point.preset_locations.random_garrison)
|
||||
position = self.location_finder.location_for(LocationType.Garrison)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
@ -366,8 +486,8 @@ class BaseDefenseGenerator:
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
def generate_sam(self) -> None:
|
||||
position = self._find_location(
|
||||
"SAM", self.control_point.preset_locations.random_base_sam)
|
||||
position = self.location_finder.location_for(
|
||||
LocationType.BaseAirDefense)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
@ -382,8 +502,7 @@ class BaseDefenseGenerator:
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
def generate_shorad(self) -> None:
|
||||
position = self._find_location(
|
||||
"SHORAD", self.control_point.preset_locations.random_garrison)
|
||||
position = self.location_finder.location_for(LocationType.Garrison)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
@ -397,34 +516,6 @@ class BaseDefenseGenerator:
|
||||
g.groups.append(group)
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
def _find_location(self, position_type: str,
|
||||
get_preset: Callable[[], None]) -> Optional[Point]:
|
||||
position = get_preset()
|
||||
if position is None:
|
||||
logging.warning(
|
||||
f"Found no preset location for {self.control_point} "
|
||||
f"{position_type}. Falling back to random location."
|
||||
)
|
||||
position = self._find_random_location()
|
||||
if position is None:
|
||||
logging.error("Could not find position for "
|
||||
f"{self.control_point} {position_type}.")
|
||||
return position
|
||||
|
||||
def _find_random_location(self) -> Optional[Point]:
|
||||
position = find_location(True, self.control_point.position,
|
||||
self.game.theater, 400, 3200, [], True)
|
||||
|
||||
# Retry once, searching a bit further (On some big airbase, 3200 is too short (Ex : Incirlik))
|
||||
# But searching farther on every base would be problematic, as some base defense units
|
||||
# would end up very far away from small airfields.
|
||||
# (I know it's not good for performance, but this is only done on campaign generation)
|
||||
# TODO : Make the whole process less stupid with preset possible positions for each airbase
|
||||
if position is None:
|
||||
position = find_location(True, self.control_point.position,
|
||||
self.game.theater, 3200, 4800, [], True)
|
||||
return position
|
||||
|
||||
|
||||
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
def __init__(self, game: Game, control_point: ControlPoint,
|
||||
@ -471,23 +562,14 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
obj_name = namegen.random_objective_name()
|
||||
template = random.choice(list(self.templates[category].values()))
|
||||
|
||||
offshore = category == "oil"
|
||||
if category == "oil":
|
||||
location_type = LocationType.OffshoreStrikeTarget
|
||||
else:
|
||||
location_type = LocationType.StrikeTarget
|
||||
|
||||
# Pick from preset locations
|
||||
location = self.pick_preset_location(offshore)
|
||||
|
||||
# Else try the old algorithm
|
||||
if location is None:
|
||||
point = find_location(not offshore,
|
||||
self.control_point.position,
|
||||
self.game.theater, 10000, 40000,
|
||||
self.control_point.ground_objects)
|
||||
else:
|
||||
point = location.position
|
||||
|
||||
point = self.location_finder.location_for(location_type)
|
||||
if point is None:
|
||||
logging.error(
|
||||
f"Could not find point for {obj_name} at {self.control_point}")
|
||||
return
|
||||
|
||||
object_id = 0
|
||||
@ -505,22 +587,8 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def generate_aa_site(self) -> None:
|
||||
obj_name = namegen.random_objective_name()
|
||||
|
||||
# Pick from preset locations
|
||||
location = self.pick_preset_location(False)
|
||||
|
||||
# If no preset location, then try the old algorithm
|
||||
if location is None:
|
||||
position = find_location(True, self.control_point.position,
|
||||
self.game.theater, 10000, 40000,
|
||||
self.control_point.ground_objects)
|
||||
else:
|
||||
position = location.position
|
||||
|
||||
position = self.location_finder.location_for(LocationType.Sam)
|
||||
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()
|
||||
@ -537,22 +605,8 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
self.generate_missile_site()
|
||||
|
||||
def generate_missile_site(self) -> None:
|
||||
|
||||
# Pick from preset locations
|
||||
location = self.pick_preset_location(False)
|
||||
|
||||
# If no preset location, then try the old algorithm
|
||||
if location is None:
|
||||
position = find_location(True, self.control_point.position,
|
||||
self.game.theater, 2500, 40000,
|
||||
[], False)
|
||||
else:
|
||||
position = location.position
|
||||
|
||||
|
||||
position = self.location_finder.location_for(LocationType.MissileSite)
|
||||
if position is None:
|
||||
logging.info(
|
||||
f"Could not find point for {self.control_point} missile site")
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
@ -591,68 +645,3 @@ class GroundObjectGenerator:
|
||||
generator = AirbaseGroundObjectGenerator(self.game, control_point,
|
||||
self.templates)
|
||||
return generator.generate()
|
||||
|
||||
|
||||
# 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 near: Point
|
||||
:param theater: Theater object
|
||||
: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_range, min_range)
|
||||
if on_ground and theater.is_on_land(p):
|
||||
point = p
|
||||
elif not on_ground and theater.is_in_sea(p):
|
||||
point = p
|
||||
|
||||
if point:
|
||||
for angle in range(0, 360, 45):
|
||||
p = point.point_from_heading(angle, 2500)
|
||||
if on_ground and not theater.is_on_land(p):
|
||||
point = None
|
||||
break
|
||||
elif not on_ground and not theater.is_in_sea(p):
|
||||
point = None
|
||||
break
|
||||
if point:
|
||||
for other in others:
|
||||
if other.position.distance_to_point(point) < 10000:
|
||||
point = None
|
||||
break
|
||||
|
||||
if point:
|
||||
for control_point in theater.controlpoints:
|
||||
if is_base_defense:
|
||||
break
|
||||
if control_point.position != near:
|
||||
if point is None:
|
||||
break
|
||||
if control_point.position.distance_to_point(point) < 30000:
|
||||
point = None
|
||||
break
|
||||
for ground_obj in control_point.ground_objects:
|
||||
if ground_obj.position.distance_to_point(point) < 10000:
|
||||
point = None
|
||||
break
|
||||
|
||||
if point:
|
||||
return point
|
||||
return None
|
||||
|
||||
@ -8,7 +8,7 @@ from gen.locations.preset_control_point_locations import PresetControlPointLocat
|
||||
from gen.locations.preset_locations import PresetLocation
|
||||
|
||||
|
||||
class PresetLocationFinder:
|
||||
class MizDataLocationFinder:
|
||||
|
||||
@staticmethod
|
||||
def compute_possible_locations(terrain_name: str, cp_name: str) -> PresetControlPointLocations:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user