mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Implement advanced skynet functions
- factor out own class for the iadsnetwork within the conflicttheater - This class will handle all Skynet related things - no specific group_name handling necessary in future - make iadsbuilding own TGO class because SAM & EWRs are Vehicle Groups. IADS Elements dont have any groups attached. - added command center, connection node and power source as Ground objects which can be added by the campaign designer - adjust lua generator to support new iads units - parse the campaign yaml to get the iads network information - use the range as fallback if no yaml information was found - complete rewrite of the skynet lua script - allow destruction of iads network to be persistent over all rounds - modified the presetlocation handling: the wrapper PresetLocation for PointWithHeading now stores the original name from the campaign miz to have the ability to process campaign yaml configurations based on the ground unit - Implementation of the UI representation for the IADS Network - Give user the option to enable or disable advanced iads - Extended the layout system: Implement Sub task handling to support PD
This commit is contained in:
@@ -15,16 +15,21 @@ from game.dcs.groundunittype import GroundUnitType
|
||||
from game.dcs.helpers import static_type_from_name
|
||||
from game.dcs.shipunittype import ShipUnitType
|
||||
from game.dcs.unittype import UnitType
|
||||
from game.theater.theatergroundobject import (
|
||||
IadsGroundObject,
|
||||
IadsBuildingGroundObject,
|
||||
NavalGroundObject,
|
||||
)
|
||||
from game.layout import LAYOUTS
|
||||
from game.layout.layout import TgoLayout, TgoLayoutGroup
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from game.theater.theatergroup import TheaterGroup
|
||||
from game.theater.theatergroup import IadsGroundGroup, IadsRole, TheaterGroup
|
||||
from game.utils import escape_string_for_lua
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import TheaterGroundObject, ControlPoint
|
||||
from game.theater import TheaterGroundObject, ControlPoint, PresetLocation
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -170,26 +175,26 @@ class ForceGroup:
|
||||
def generate(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
location: PresetLocation,
|
||||
control_point: ControlPoint,
|
||||
game: Game,
|
||||
) -> TheaterGroundObject:
|
||||
"""Create a random TheaterGroundObject from the available templates"""
|
||||
layout = random.choice(self.layouts)
|
||||
return self.create_ground_object_for_layout(
|
||||
layout, name, position, control_point, game
|
||||
layout, name, location, control_point, game
|
||||
)
|
||||
|
||||
def create_ground_object_for_layout(
|
||||
self,
|
||||
layout: TgoLayout,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
location: PresetLocation,
|
||||
control_point: ControlPoint,
|
||||
game: Game,
|
||||
) -> TheaterGroundObject:
|
||||
"""Create a TheaterGroundObject for the given template"""
|
||||
go = layout.create_ground_object(name, position, control_point)
|
||||
go = layout.create_ground_object(name, location, control_point)
|
||||
# Generate all groups using the randomization if it defined
|
||||
for group_name, groups in layout.groups.items():
|
||||
for group in groups:
|
||||
@@ -223,7 +228,11 @@ class ForceGroup:
|
||||
"""Create a TheaterGroup and add it to the given TGO"""
|
||||
# Random UnitCounter if not forced
|
||||
if unit_count is None:
|
||||
# Choose a random group_size based on the layouts unit_count
|
||||
unit_count = group.group_size
|
||||
if unit_count == 0:
|
||||
# No units to be created so dont create a theater group for them
|
||||
return
|
||||
# Generate Units
|
||||
units = group.generate_units(ground_object, unit_type, unit_count)
|
||||
# Get or create the TheaterGroup
|
||||
@@ -233,16 +242,27 @@ class ForceGroup:
|
||||
ground_group.units.extend(units)
|
||||
else:
|
||||
# TheaterGroup with the name was not created yet
|
||||
ground_object.groups.append(
|
||||
TheaterGroup.from_template(
|
||||
game.next_group_id(),
|
||||
group_name,
|
||||
units,
|
||||
ground_object,
|
||||
unit_type,
|
||||
unit_count,
|
||||
)
|
||||
ground_group = TheaterGroup.from_template(
|
||||
game.next_group_id(), group_name, units, ground_object
|
||||
)
|
||||
# Special handling when part of the IADS (SAM, EWR, IADS Building, Navy)
|
||||
if (
|
||||
isinstance(ground_object, IadsGroundObject)
|
||||
or isinstance(ground_object, IadsBuildingGroundObject)
|
||||
or isinstance(ground_object, NavalGroundObject)
|
||||
):
|
||||
# Recreate the TheaterGroup as IadsGroundGroup
|
||||
ground_group = IadsGroundGroup.from_group(ground_group)
|
||||
if group.sub_task is not None:
|
||||
# Use the special sub_task of the TheaterGroup
|
||||
iads_task = group.sub_task
|
||||
else:
|
||||
# Use the primary task of the ForceGroup
|
||||
iads_task = self.tasks[0]
|
||||
# Set the iads_role according the the task for the group
|
||||
ground_group.iads_role = IadsRole.for_task(iads_task)
|
||||
|
||||
ground_object.groups.append(ground_group)
|
||||
|
||||
# A layout has to be created with an orientation of 0 deg.
|
||||
# Therefore the the clockwise rotation angle is always the heading of the
|
||||
|
||||
@@ -22,6 +22,7 @@ from game.theater import (
|
||||
SyriaTheater,
|
||||
TheChannelTheater,
|
||||
)
|
||||
from game.theater.iadsnetwork.iadsnetwork import IadsNetwork
|
||||
from game.version import CAMPAIGN_FORMAT_VERSION
|
||||
from .campaignairwingconfig import CampaignAirWingConfig
|
||||
from .mizcampaignloader import MizCampaignLoader
|
||||
@@ -58,6 +59,7 @@ class Campaign:
|
||||
performance: int
|
||||
data: Dict[str, Any]
|
||||
path: Path
|
||||
advanced_iads: bool
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path: Path) -> Campaign:
|
||||
@@ -109,9 +111,10 @@ class Campaign:
|
||||
data.get("performance", 0),
|
||||
data,
|
||||
path,
|
||||
data.get("advanced_iads", False),
|
||||
)
|
||||
|
||||
def load_theater(self) -> ConflictTheater:
|
||||
def load_theater(self, advanced_iads: bool) -> ConflictTheater:
|
||||
theaters = {
|
||||
"Caucasus": CaucasusTheater,
|
||||
"Nevada": NevadaTheater,
|
||||
@@ -133,6 +136,10 @@ class Campaign:
|
||||
|
||||
with logged_duration("Importing miz data"):
|
||||
MizCampaignLoader(self.path.parent / miz, t).populate_theater()
|
||||
|
||||
# Load IADS Config from campaign yaml
|
||||
iads_data = self.data.get("iads_config", [])
|
||||
t.iads_network = IadsNetwork(advanced_iads, iads_data)
|
||||
return t
|
||||
|
||||
def load_air_wing_config(self, theater: ConflictTheater) -> CampaignAirWingConfig:
|
||||
|
||||
@@ -16,10 +16,11 @@ from dcs.terrain import Airport
|
||||
from dcs.unitgroup import PlaneGroup, ShipGroup, StaticGroup, VehicleGroup
|
||||
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
|
||||
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from game.positioned import Positioned
|
||||
from game.profiling import logged_duration
|
||||
from game.scenery_group import SceneryGroup
|
||||
from game.theater.presetlocation import PresetLocation
|
||||
from game.utils import Distance, meters
|
||||
from game.theater.controlpoint import (
|
||||
Airfield,
|
||||
Carrier,
|
||||
@@ -53,6 +54,10 @@ class MizCampaignLoader:
|
||||
MISSILE_SITE_UNIT_TYPE = MissilesSS.Scud_B.id
|
||||
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.Hy_launcher.id
|
||||
|
||||
COMMAND_CENTER_UNIT_TYPE = Fortification._Command_Center.id
|
||||
CONNECTION_NODE_UNIT_TYPE = Fortification.Comms_tower_M.id
|
||||
POWER_SOURCE_UNIT_TYPE = Fortification.GeneratorF.id
|
||||
|
||||
# Multiple options for air defenses so campaign designers can more accurately see
|
||||
# the coverage of their IADS for the expected type.
|
||||
LONG_RANGE_SAM_UNIT_TYPES = {
|
||||
@@ -279,6 +284,24 @@ class MizCampaignLoader:
|
||||
if group.units[0].type == self.SHIPPING_LANE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def iads_command_centers(self) -> Iterator[StaticGroup]:
|
||||
for group in itertools.chain(self.blue.static_group, self.red.static_group):
|
||||
if group.units[0].type in self.COMMAND_CENTER_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def iads_connection_nodes(self) -> Iterator[StaticGroup]:
|
||||
for group in itertools.chain(self.blue.static_group, self.red.static_group):
|
||||
if group.units[0].type in self.CONNECTION_NODE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def iads_power_sources(self) -> Iterator[StaticGroup]:
|
||||
for group in itertools.chain(self.blue.static_group, self.red.static_group):
|
||||
if group.units[0].type in self.POWER_SOURCE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
def add_supply_routes(self) -> None:
|
||||
for group in self.front_line_path_groups:
|
||||
# The unit will have its first waypoint at the source CP and the final
|
||||
@@ -334,113 +357,93 @@ class MizCampaignLoader:
|
||||
for static in self.offshore_strike_targets:
|
||||
closest, distance = self.objective_info(static)
|
||||
closest.preset_locations.offshore_strike_locations.append(
|
||||
PointWithHeading.from_point(
|
||||
static.position, Heading.from_degrees(static.units[0].heading)
|
||||
)
|
||||
PresetLocation.from_group(static)
|
||||
)
|
||||
|
||||
for ship in self.ships:
|
||||
closest, distance = self.objective_info(ship, allow_naval=True)
|
||||
closest.preset_locations.ships.append(
|
||||
PointWithHeading.from_point(
|
||||
ship.position, Heading.from_degrees(ship.units[0].heading)
|
||||
)
|
||||
)
|
||||
closest.preset_locations.ships.append(PresetLocation.from_group(ship))
|
||||
|
||||
for group in self.missile_sites:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.missile_sites.append(
|
||||
PointWithHeading.from_point(
|
||||
group.position, Heading.from_degrees(group.units[0].heading)
|
||||
)
|
||||
PresetLocation.from_group(group)
|
||||
)
|
||||
|
||||
for group in self.coastal_defenses:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.coastal_defenses.append(
|
||||
PointWithHeading.from_point(
|
||||
group.position, Heading.from_degrees(group.units[0].heading)
|
||||
)
|
||||
PresetLocation.from_group(group)
|
||||
)
|
||||
|
||||
for group in self.long_range_sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.long_range_sams.append(
|
||||
PointWithHeading.from_point(
|
||||
group.position, Heading.from_degrees(group.units[0].heading)
|
||||
)
|
||||
PresetLocation.from_group(group)
|
||||
)
|
||||
|
||||
for group in self.medium_range_sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.medium_range_sams.append(
|
||||
PointWithHeading.from_point(
|
||||
group.position, Heading.from_degrees(group.units[0].heading)
|
||||
)
|
||||
PresetLocation.from_group(group)
|
||||
)
|
||||
|
||||
for group in self.short_range_sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.short_range_sams.append(
|
||||
PointWithHeading.from_point(
|
||||
group.position, Heading.from_degrees(group.units[0].heading)
|
||||
)
|
||||
PresetLocation.from_group(group)
|
||||
)
|
||||
|
||||
for group in self.aaa:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.aaa.append(
|
||||
PointWithHeading.from_point(
|
||||
group.position, Heading.from_degrees(group.units[0].heading)
|
||||
)
|
||||
)
|
||||
closest.preset_locations.aaa.append(PresetLocation.from_group(group))
|
||||
|
||||
for group in self.ewrs:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.ewrs.append(
|
||||
PointWithHeading.from_point(
|
||||
group.position, Heading.from_degrees(group.units[0].heading)
|
||||
)
|
||||
)
|
||||
closest.preset_locations.ewrs.append(PresetLocation.from_group(group))
|
||||
|
||||
for group in self.armor_groups:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.armor_groups.append(
|
||||
PointWithHeading.from_point(
|
||||
group.position, Heading.from_degrees(group.units[0].heading)
|
||||
)
|
||||
PresetLocation.from_group(group)
|
||||
)
|
||||
|
||||
for static in self.helipads:
|
||||
closest, distance = self.objective_info(static)
|
||||
closest.helipads.append(
|
||||
PointWithHeading.from_point(
|
||||
static.position, Heading.from_degrees(static.units[0].heading)
|
||||
)
|
||||
)
|
||||
closest.helipads.append(PresetLocation.from_group(static))
|
||||
|
||||
for static in self.factories:
|
||||
closest, distance = self.objective_info(static)
|
||||
closest.preset_locations.factories.append(
|
||||
PointWithHeading.from_point(
|
||||
static.position, Heading.from_degrees(static.units[0].heading)
|
||||
)
|
||||
)
|
||||
closest.preset_locations.factories.append(PresetLocation.from_group(static))
|
||||
|
||||
for static in self.ammunition_depots:
|
||||
closest, distance = self.objective_info(static)
|
||||
closest.preset_locations.ammunition_depots.append(
|
||||
PointWithHeading.from_point(
|
||||
static.position, Heading.from_degrees(static.units[0].heading)
|
||||
)
|
||||
PresetLocation.from_group(static)
|
||||
)
|
||||
|
||||
for static in self.strike_targets:
|
||||
closest, distance = self.objective_info(static)
|
||||
closest.preset_locations.strike_locations.append(
|
||||
PointWithHeading.from_point(
|
||||
static.position, Heading.from_degrees(static.units[0].heading)
|
||||
)
|
||||
PresetLocation.from_group(static)
|
||||
)
|
||||
|
||||
for iads_command_center in self.iads_command_centers:
|
||||
closest, distance = self.objective_info(iads_command_center)
|
||||
closest.preset_locations.iads_command_center.append(
|
||||
PresetLocation.from_group(iads_command_center)
|
||||
)
|
||||
|
||||
for iads_connection_node in self.iads_connection_nodes:
|
||||
closest, distance = self.objective_info(iads_connection_node)
|
||||
closest.preset_locations.iads_connection_node.append(
|
||||
PresetLocation.from_group(iads_connection_node)
|
||||
)
|
||||
|
||||
for iads_power_source in self.iads_power_sources:
|
||||
closest, distance = self.objective_info(iads_power_source)
|
||||
closest.preset_locations.iads_power_source.append(
|
||||
PresetLocation.from_group(iads_power_source)
|
||||
)
|
||||
|
||||
for scenery_group in self.scenery:
|
||||
|
||||
@@ -17,6 +17,7 @@ from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
IadsGroundObject,
|
||||
NavalGroundObject,
|
||||
IadsBuildingGroundObject,
|
||||
)
|
||||
from game.utils import meters, nautical_miles
|
||||
from game.ato.closestairfields import ClosestAirfields, ObjectiveDistanceCache
|
||||
@@ -114,6 +115,13 @@ class ObjectiveFinder:
|
||||
# AI.
|
||||
continue
|
||||
|
||||
if isinstance(
|
||||
ground_object, IadsBuildingGroundObject
|
||||
) and not self.game.settings.plugin_option("skynetiads"):
|
||||
# Prevent strike targets on IADS Buildings when skynet features
|
||||
# are disabled as they do not serve any purpose
|
||||
continue
|
||||
|
||||
if ground_object.is_dead:
|
||||
continue
|
||||
if ground_object.name in found_targets:
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
RUNWAY_REPAIR_COST = 100
|
||||
|
||||
REWARDS = {
|
||||
"power": 4,
|
||||
"warehouse": 2,
|
||||
"ware": 2,
|
||||
"fuel": 2,
|
||||
@@ -13,7 +12,6 @@ REWARDS = {
|
||||
# TODO: Should generate no cash once they generate units.
|
||||
# https://github.com/dcs-liberation/dcs_liberation/issues/1036
|
||||
"factory": 10,
|
||||
"comms": 10,
|
||||
"oil": 10,
|
||||
"derrick": 8,
|
||||
"village": 0.25,
|
||||
|
||||
@@ -7,13 +7,17 @@ REQUIRED_BUILDINGS = [
|
||||
"fob",
|
||||
]
|
||||
|
||||
IADS_BUILDINGS = [
|
||||
"comms",
|
||||
"power",
|
||||
"commandcenter",
|
||||
]
|
||||
|
||||
DEFAULT_AVAILABLE_BUILDINGS = [
|
||||
"fuel",
|
||||
"comms",
|
||||
"oil",
|
||||
"ware",
|
||||
"farp",
|
||||
"power",
|
||||
"derrick",
|
||||
]
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ class GroupTask(Enum):
|
||||
LORAD = ("LORAD", GroupRole.AIR_DEFENSE)
|
||||
MERAD = ("MERAD", GroupRole.AIR_DEFENSE)
|
||||
SHORAD = ("SHORAD", GroupRole.AIR_DEFENSE)
|
||||
POINT_DEFENSE = ("PointDefense", GroupRole.AIR_DEFENSE)
|
||||
|
||||
# NAVAL
|
||||
AIRCRAFT_CARRIER = ("AircraftCarrier", GroupRole.NAVAL)
|
||||
@@ -54,7 +55,6 @@ class GroupTask(Enum):
|
||||
# BUILDINGS
|
||||
ALLY_CAMP = ("AllyCamp", GroupRole.BUILDING)
|
||||
AMMO = ("Ammo", GroupRole.BUILDING)
|
||||
COMMS = ("Comms", GroupRole.BUILDING)
|
||||
DERRICK = ("Derrick", GroupRole.BUILDING)
|
||||
FACTORY = ("Factory", GroupRole.BUILDING)
|
||||
FARP = ("Farp", GroupRole.BUILDING)
|
||||
@@ -62,8 +62,13 @@ class GroupTask(Enum):
|
||||
FUEL = ("Fuel", GroupRole.BUILDING)
|
||||
OFFSHORE_STRIKE_TARGET = ("OffShoreStrikeTarget", GroupRole.BUILDING)
|
||||
OIL = ("Oil", GroupRole.BUILDING)
|
||||
POWER = ("Power", GroupRole.BUILDING)
|
||||
|
||||
STRIKE_TARGET = ("StrikeTarget", GroupRole.BUILDING)
|
||||
VILLAGE = ("Village", GroupRole.BUILDING)
|
||||
WARE = ("Ware", GroupRole.BUILDING)
|
||||
WW2_BUNKER = ("WW2Bunker", GroupRole.BUILDING)
|
||||
|
||||
# IADS
|
||||
COMMS = ("Comms", GroupRole.BUILDING)
|
||||
COMMAND_CENTER = ("CommandCenter", GroupRole.BUILDING)
|
||||
POWER = ("Power", GroupRole.BUILDING)
|
||||
|
||||
@@ -17,6 +17,7 @@ from game.data.building_data import (
|
||||
WW2_GERMANY_BUILDINGS,
|
||||
WW2_FREE,
|
||||
REQUIRED_BUILDINGS,
|
||||
IADS_BUILDINGS,
|
||||
)
|
||||
from game.data.doctrine import (
|
||||
Doctrine,
|
||||
@@ -256,6 +257,7 @@ class Faction:
|
||||
|
||||
# Add required buildings for the game logic (e.g. ammo, factory..)
|
||||
faction.building_set.extend(REQUIRED_BUILDINGS)
|
||||
faction.building_set.extend(IADS_BUILDINGS)
|
||||
|
||||
# Load liveries override
|
||||
faction.liveries_overrides = {}
|
||||
|
||||
@@ -277,6 +277,10 @@ class Game:
|
||||
"""Initialization for the first turn of the game."""
|
||||
from .sim import GameUpdateEvents
|
||||
|
||||
# Build the IADS Network
|
||||
with logged_duration("Generate IADS Network"):
|
||||
self.theater.iads_network.initialize_network(self.theater.ground_objects)
|
||||
|
||||
for control_point in self.theater.controlpoints:
|
||||
control_point.initialize_turn_0()
|
||||
for tgo in control_point.connected_objectives:
|
||||
|
||||
@@ -4,7 +4,7 @@ from collections import defaultdict
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Iterator, Type
|
||||
from typing import TYPE_CHECKING, Iterator, Type, Optional
|
||||
|
||||
from dcs import Point
|
||||
from dcs.unit import Unit
|
||||
@@ -12,8 +12,10 @@ from dcs.unittype import UnitType as DcsUnitType
|
||||
|
||||
from game.data.groups import GroupRole, GroupTask
|
||||
from game.data.units import UnitClass
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from game.theater.iadsnetwork.iadsrole import IadsRole
|
||||
from game.theater.presetlocation import PresetLocation
|
||||
from game.theater.theatergroundobject import (
|
||||
IadsBuildingGroundObject,
|
||||
SamGroundObject,
|
||||
EwrGroundObject,
|
||||
BuildingGroundObject,
|
||||
@@ -93,6 +95,9 @@ class TgoLayoutGroup:
|
||||
unit_classes: list[UnitClass] = field(default_factory=list)
|
||||
fallback_classes: list[UnitClass] = field(default_factory=list)
|
||||
|
||||
# Allows a group to have a special SubTask (PointDefence for example)
|
||||
sub_task: Optional[GroupTask] = None
|
||||
|
||||
# Defines if this groupTemplate is required or not
|
||||
optional: bool = False
|
||||
|
||||
@@ -203,7 +208,7 @@ class TgoLayout:
|
||||
def create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
location: PresetLocation,
|
||||
control_point: ControlPoint,
|
||||
) -> TheaterGroundObject:
|
||||
"""Create the TheaterGroundObject for the TgoLayout
|
||||
@@ -223,14 +228,14 @@ class AntiAirLayout(TgoLayout):
|
||||
def create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
location: PresetLocation,
|
||||
control_point: ControlPoint,
|
||||
) -> IadsGroundObject:
|
||||
|
||||
if GroupTask.EARLY_WARNING_RADAR in self.tasks:
|
||||
return EwrGroundObject(name, position, position.heading, control_point)
|
||||
return EwrGroundObject(name, location, control_point)
|
||||
elif any(tasking in self.tasks for tasking in GroupRole.AIR_DEFENSE.tasks):
|
||||
return SamGroundObject(name, position, position.heading, control_point)
|
||||
return SamGroundObject(name, location, control_point)
|
||||
raise RuntimeError(
|
||||
f" No Template for AntiAir tasking ({', '.join(task.description for task in self.tasks)})"
|
||||
)
|
||||
@@ -240,14 +245,17 @@ class BuildingLayout(TgoLayout):
|
||||
def create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
location: PresetLocation,
|
||||
control_point: ControlPoint,
|
||||
) -> BuildingGroundObject:
|
||||
return BuildingGroundObject(
|
||||
iads_role = IadsRole.for_category(self.category)
|
||||
tgo_type = (
|
||||
IadsBuildingGroundObject if iads_role.participate else BuildingGroundObject
|
||||
)
|
||||
return tgo_type(
|
||||
name,
|
||||
self.category,
|
||||
position,
|
||||
position.heading,
|
||||
location,
|
||||
control_point,
|
||||
self.category == "fob",
|
||||
)
|
||||
@@ -264,15 +272,15 @@ class NavalLayout(TgoLayout):
|
||||
def create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
location: PresetLocation,
|
||||
control_point: ControlPoint,
|
||||
) -> TheaterGroundObject:
|
||||
if GroupTask.NAVY in self.tasks:
|
||||
return ShipGroundObject(name, position, control_point)
|
||||
return ShipGroundObject(name, location, control_point)
|
||||
elif GroupTask.AIRCRAFT_CARRIER in self.tasks:
|
||||
return CarrierGroundObject(name, control_point)
|
||||
return CarrierGroundObject(name, location, control_point)
|
||||
elif GroupTask.HELICOPTER_CARRIER in self.tasks:
|
||||
return LhaGroundObject(name, control_point)
|
||||
return LhaGroundObject(name, location, control_point)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -280,17 +288,13 @@ class DefensesLayout(TgoLayout):
|
||||
def create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
location: PresetLocation,
|
||||
control_point: ControlPoint,
|
||||
) -> TheaterGroundObject:
|
||||
if GroupTask.MISSILE in self.tasks:
|
||||
return MissileSiteGroundObject(
|
||||
name, position, position.heading, control_point
|
||||
)
|
||||
return MissileSiteGroundObject(name, location, control_point)
|
||||
elif GroupTask.COASTAL in self.tasks:
|
||||
return CoastalSiteGroundObject(
|
||||
name, position, control_point, position.heading
|
||||
)
|
||||
return CoastalSiteGroundObject(name, location, control_point)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -298,7 +302,7 @@ class GroundForceLayout(TgoLayout):
|
||||
def create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
location: PresetLocation,
|
||||
control_point: ControlPoint,
|
||||
) -> TheaterGroundObject:
|
||||
return VehicleGroupGroundObject(name, position, position.heading, control_point)
|
||||
return VehicleGroupGroundObject(name, location, control_point)
|
||||
|
||||
@@ -167,6 +167,7 @@ class LayoutLoader:
|
||||
)
|
||||
group_layout.optional = group_mapping.optional
|
||||
group_layout.fill = group_mapping.fill
|
||||
group_layout.sub_task = group_mapping.sub_task
|
||||
# Add the group at the correct index
|
||||
layout.add_layout_group(group_name, group_layout, g_id)
|
||||
layout_unit = LayoutUnit.from_unit(unit)
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Type
|
||||
from typing import Any, Optional, Type
|
||||
|
||||
from dcs.unittype import UnitType as DcsUnitType
|
||||
|
||||
@@ -22,6 +22,9 @@ class GroupLayoutMapping:
|
||||
# Should this be filled by accessible units if optional or not
|
||||
fill: bool = True
|
||||
|
||||
# Allows a group to have a special SubTask (PointDefence for example)
|
||||
sub_task: Optional[GroupTask] = None
|
||||
|
||||
# All static units for the group
|
||||
statics: list[str] = field(default_factory=list)
|
||||
|
||||
@@ -44,6 +47,7 @@ class GroupLayoutMapping:
|
||||
def from_dict(d: dict[str, Any]) -> GroupLayoutMapping:
|
||||
optional = d["optional"] if "optional" in d else False
|
||||
fill = d["fill"] if "fill" in d else True
|
||||
sub_task = GroupTask.by_description(d["sub_task"]) if "sub_task" in d else None
|
||||
statics = d["statics"] if "statics" in d else []
|
||||
unit_count = d["unit_count"] if "unit_count" in d else []
|
||||
unit_types = []
|
||||
@@ -64,6 +68,7 @@ class GroupLayoutMapping:
|
||||
d["name"],
|
||||
optional,
|
||||
fill,
|
||||
sub_task,
|
||||
statics,
|
||||
unit_count,
|
||||
unit_types,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
|
||||
import logging
|
||||
import os
|
||||
@@ -134,6 +135,16 @@ class LuaGenerator:
|
||||
"positionY", str(ground_object.position.y)
|
||||
)
|
||||
|
||||
# Generate IADS Lua Item
|
||||
iads_object = lua_data.add_item("IADS")
|
||||
for node in self.game.theater.iads_network.skynet_nodes(self.game):
|
||||
coalition = iads_object.get_or_create_item("BLUE" if node.player else "RED")
|
||||
iads_type = coalition.get_or_create_item(node.iads_role.value)
|
||||
iads_element = iads_type.add_item()
|
||||
iads_element.add_key_value("dcsGroupName", node.dcs_name)
|
||||
for role, connections in node.connections.items():
|
||||
iads_element.add_data_array(role, connections)
|
||||
|
||||
trigger = TriggerStart(comment="Set DCS Liberation data")
|
||||
trigger.add_action(DoScript(String(lua_data.create_operations_lua())))
|
||||
self.mission.triggerrules.triggers.append(trigger)
|
||||
@@ -183,11 +194,6 @@ class LuaValue:
|
||||
self.key = key
|
||||
self.value = value
|
||||
|
||||
def _escape_value(self, value: str) -> str:
|
||||
value = value.replace('"', "'") # Replace Double Quote as this is the delimiter
|
||||
value = value.replace(os.sep, "/") # Replace Backslash as path separator
|
||||
return '"{0}"'.format(value)
|
||||
|
||||
def serialize(self) -> str:
|
||||
serialized_value = self.key + " = " if self.key else ""
|
||||
if isinstance(self.value, str):
|
||||
@@ -255,20 +261,17 @@ class LuaData(LuaItem):
|
||||
super().__init__(name)
|
||||
|
||||
def add_item(self, item_name: Optional[str] = None) -> LuaItem:
|
||||
"""adds a new item to the LuaArray without checking the existence"""
|
||||
item = LuaData(item_name, False)
|
||||
self.objects.append(item)
|
||||
return item
|
||||
|
||||
def get_item(self, item_name: str) -> Optional[LuaItem]:
|
||||
"""gets item from LuaArray. Returns None if it does not exist"""
|
||||
for lua_object in self.objects:
|
||||
if lua_object.name == item_name:
|
||||
return lua_object
|
||||
return None
|
||||
|
||||
def get_or_create_item(self, item_name: Optional[str] = None) -> LuaItem:
|
||||
"""gets item from the LuaArray or creates one if it does not exist already"""
|
||||
if item_name:
|
||||
item = self.get_item(item_name)
|
||||
if item:
|
||||
|
||||
@@ -12,6 +12,7 @@ import random
|
||||
from collections import defaultdict
|
||||
from typing import Any, Dict, Iterator, List, Optional, TYPE_CHECKING, Type
|
||||
|
||||
import dcs.vehicles
|
||||
from dcs import Mission, Point, unitgroup
|
||||
from dcs.action import DoScript, SceneryDestructionZone
|
||||
from dcs.condition import MapObjectIsDead
|
||||
@@ -43,7 +44,7 @@ from game.theater.theatergroundobject import (
|
||||
LhaGroundObject,
|
||||
MissileSiteGroundObject,
|
||||
)
|
||||
from game.theater.theatergroup import SceneryUnit, TheaterGroup
|
||||
from game.theater.theatergroup import SceneryUnit, TheaterGroup, IadsGroundGroup
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import Heading, feet, knots, mps
|
||||
|
||||
@@ -84,8 +85,19 @@ class GroundObjectGenerator:
|
||||
# Split the different unit types to be compliant to dcs limitation
|
||||
for unit in group.units:
|
||||
if unit.is_static:
|
||||
# A Static unit has to be a single static group
|
||||
self.create_static_group(unit)
|
||||
if isinstance(unit, SceneryUnit):
|
||||
# Special handling for scenery objects
|
||||
self.add_trigger_zone_for_scenery(unit)
|
||||
if (
|
||||
self.game.settings.plugin_option("skynetiads")
|
||||
and isinstance(group, IadsGroundGroup)
|
||||
and group.iads_role.participate
|
||||
):
|
||||
# Generate a unit which can be controlled by skynet
|
||||
self.generate_iads_command_unit(unit)
|
||||
else:
|
||||
# Create a static group for each static unit
|
||||
self.create_static_group(unit)
|
||||
elif unit.is_vehicle and unit.alive:
|
||||
# All alive Vehicles
|
||||
vehicle_units.append(unit)
|
||||
@@ -160,12 +172,6 @@ class GroundObjectGenerator:
|
||||
return ship_group
|
||||
|
||||
def create_static_group(self, unit: TheaterUnit) -> None:
|
||||
if isinstance(unit, SceneryUnit):
|
||||
# Special handling for scenery objects:
|
||||
# Only create a trigger zone and no "real" dcs unit
|
||||
self.add_trigger_zone_for_scenery(unit)
|
||||
return
|
||||
|
||||
static_group = self.m.static_group(
|
||||
country=self.country,
|
||||
name=unit.unit_name,
|
||||
@@ -243,6 +249,19 @@ class GroundObjectGenerator:
|
||||
t.actions.append(DoScript(script_string))
|
||||
self.m.triggerrules.triggers.append(t)
|
||||
|
||||
def generate_iads_command_unit(self, unit: SceneryUnit) -> None:
|
||||
# Creates a static Infantry Unit next to a scenery object. This is needed
|
||||
# because skynet can not use map objects as Comms, Power or Command and needs a
|
||||
# "real" unit to function correctly
|
||||
self.m.static_group(
|
||||
country=self.country,
|
||||
name=unit.unit_name,
|
||||
_type=dcs.vehicles.Infantry.Soldier_M4,
|
||||
position=unit.position,
|
||||
heading=unit.position.heading.degrees,
|
||||
dead=not unit.alive, # Also spawn as dead!
|
||||
)
|
||||
|
||||
|
||||
class MissileSiteGenerator(GroundObjectGenerator):
|
||||
@property
|
||||
|
||||
@@ -14,6 +14,7 @@ from . import (
|
||||
supplyroutes,
|
||||
tgos,
|
||||
waypoints,
|
||||
iadsnetwork,
|
||||
)
|
||||
from .settings import ServerSettings
|
||||
|
||||
@@ -30,6 +31,7 @@ app.include_router(qt.router)
|
||||
app.include_router(supplyroutes.router)
|
||||
app.include_router(tgos.router)
|
||||
app.include_router(waypoints.router)
|
||||
app.include_router(iadsnetwork.router)
|
||||
|
||||
|
||||
origins = []
|
||||
|
||||
@@ -12,6 +12,7 @@ from game.server.mapzones.models import ThreatZoneContainerJs
|
||||
from game.server.navmesh.models import NavMeshesJs
|
||||
from game.server.supplyroutes.models import SupplyRouteJs
|
||||
from game.server.tgos.models import TgoJs
|
||||
from game.server.iadsnetwork.models import IadsConnectionJs, IadsNetworkJs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@@ -23,6 +24,7 @@ class GameJs(BaseModel):
|
||||
supply_routes: list[SupplyRouteJs]
|
||||
front_lines: list[FrontLineJs]
|
||||
flights: list[FlightJs]
|
||||
iads_network: IadsNetworkJs
|
||||
threat_zones: ThreatZoneContainerJs
|
||||
navmeshes: NavMeshesJs
|
||||
map_center: LeafletPoint | None
|
||||
@@ -38,6 +40,7 @@ class GameJs(BaseModel):
|
||||
supply_routes=SupplyRouteJs.all_in_game(game),
|
||||
front_lines=FrontLineJs.all_in_game(game),
|
||||
flights=FlightJs.all_in_game(game, with_waypoints=True),
|
||||
iads_network=IadsNetworkJs.from_network(game.theater.iads_network),
|
||||
threat_zones=ThreatZoneContainerJs.for_game(game),
|
||||
navmeshes=NavMeshesJs.from_game(game),
|
||||
map_center=game.theater.terrain.map_view_default.position.latlng(),
|
||||
|
||||
1
game/server/iadsnetwork/__init__.py
Normal file
1
game/server/iadsnetwork/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .routes import router
|
||||
76
game/server/iadsnetwork/models.py
Normal file
76
game/server/iadsnetwork/models.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from __future__ import annotations
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from game.server.leaflet import LeafletPoint
|
||||
from game.theater.iadsnetwork.iadsnetwork import IadsNetworkNode, IadsNetwork
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
|
||||
class IadsConnectionJs(BaseModel):
|
||||
id: UUID
|
||||
points: list[LeafletPoint]
|
||||
node: UUID
|
||||
connected: UUID
|
||||
active: bool
|
||||
blue: bool
|
||||
is_power: bool
|
||||
|
||||
class Config:
|
||||
title = "IadsConnection"
|
||||
|
||||
@staticmethod
|
||||
def connections_for_tgo(
|
||||
tgo_id: UUID, network: IadsNetwork
|
||||
) -> list[IadsConnectionJs]:
|
||||
for node in network.nodes:
|
||||
if node.group.ground_object.id == tgo_id:
|
||||
return IadsConnectionJs.connections_for_node(node)
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def connections_for_node(network_node: IadsNetworkNode) -> list[IadsConnectionJs]:
|
||||
iads_connections = []
|
||||
tgo = network_node.group.ground_object
|
||||
for id, connection in network_node.connections.items():
|
||||
if connection.ground_object.is_friendly(True) != tgo.is_friendly(True):
|
||||
continue # Skip connections which are not from same coalition
|
||||
iads_connections.append(
|
||||
IadsConnectionJs(
|
||||
id=id,
|
||||
points=[
|
||||
tgo.position.latlng(),
|
||||
connection.ground_object.position.latlng(),
|
||||
],
|
||||
node=tgo.id,
|
||||
connected=connection.ground_object.id,
|
||||
active=(
|
||||
tgo.alive_unit_count > 0
|
||||
and connection.ground_object.alive_unit_count > 0
|
||||
),
|
||||
blue=tgo.is_friendly(True),
|
||||
is_power="power"
|
||||
in [tgo.category, connection.ground_object.category],
|
||||
)
|
||||
)
|
||||
return iads_connections
|
||||
|
||||
|
||||
class IadsNetworkJs(BaseModel):
|
||||
advanced: bool
|
||||
connections: list[IadsConnectionJs]
|
||||
|
||||
class Config:
|
||||
title = "IadsNetwork"
|
||||
|
||||
@staticmethod
|
||||
def from_network(network: IadsNetwork) -> IadsNetworkJs:
|
||||
iads_connections = []
|
||||
for connection in network.nodes:
|
||||
if not connection.group.iads_role.participate:
|
||||
continue # Skip
|
||||
iads_connections.extend(IadsConnectionJs.connections_for_node(connection))
|
||||
return IadsNetworkJs(
|
||||
advanced=network.advanced_iads, connections=iads_connections
|
||||
)
|
||||
26
game/server/iadsnetwork/routes.py
Normal file
26
game/server/iadsnetwork/routes.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from uuid import UUID
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from game import Game
|
||||
from .models import IadsConnectionJs, IadsNetworkJs
|
||||
from ..dependencies import GameContext
|
||||
|
||||
router: APIRouter = APIRouter(prefix="/iads-network")
|
||||
|
||||
|
||||
@router.get("/", operation_id="get_iads_network", response_model=IadsNetworkJs)
|
||||
def get_iads_network(
|
||||
game: Game = Depends(GameContext.require),
|
||||
) -> IadsNetworkJs:
|
||||
return IadsNetworkJs.from_network(game.theater.iads_network)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/for-tgo/{tgo_id}",
|
||||
operation_id="get_iads_connections_for_tgo",
|
||||
response_model=list[IadsConnectionJs],
|
||||
)
|
||||
def get_iads_connections_for_tgo(
|
||||
tgo_id: UUID, game: Game = Depends(GameContext.require)
|
||||
) -> list[IadsConnectionJs]:
|
||||
return IadsConnectionJs.connections_for_tgo(tgo_id, game.theater.iads_network)
|
||||
@@ -260,6 +260,7 @@ class LandInstallationEntity(Entity):
|
||||
GENERATION_STATION = 120502
|
||||
PETROLEUM_FACILITY = 120504
|
||||
MILITARY_BASE = 120802
|
||||
MILITARY_INFRASTRUCTURE = 120800
|
||||
PUBLIC_VENUES_INFRASTRUCTURE = 121000
|
||||
TELECOMMUNICATIONS_TOWER = 121203
|
||||
AIPORT_AIR_BASE = 121301
|
||||
|
||||
@@ -21,6 +21,7 @@ from dcs.terrain.terrain import Terrain
|
||||
from shapely import geometry, ops
|
||||
|
||||
from .frontline import FrontLine
|
||||
from .iadsnetwork.iadsnetwork import IadsNetwork
|
||||
from .landmap import Landmap, load_landmap, poly_contains
|
||||
from .seasonalconditions import SeasonalConditions
|
||||
from ..utils import Heading
|
||||
@@ -45,6 +46,7 @@ class ConflictTheater:
|
||||
land_poly = None # type: Polygon
|
||||
"""
|
||||
daytime_map: Dict[str, Tuple[int, int]]
|
||||
iads_network: IadsNetwork
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.controlpoints: List[ControlPoint] = []
|
||||
@@ -57,6 +59,12 @@ class ConflictTheater:
|
||||
def add_controlpoint(self, point: ControlPoint) -> None:
|
||||
self.controlpoints.append(point)
|
||||
|
||||
@property
|
||||
def ground_objects(self) -> Iterator[TheaterGroundObject]:
|
||||
for cp in self.controlpoints:
|
||||
for go in cp.ground_objects:
|
||||
yield go
|
||||
|
||||
def find_ground_objects_by_obj_name(
|
||||
self, obj_name: str
|
||||
) -> list[TheaterGroundObject]:
|
||||
|
||||
@@ -57,6 +57,7 @@ from game.sidc import (
|
||||
SymbolSet,
|
||||
)
|
||||
from game.utils import Distance, Heading, meters
|
||||
from game.theater.presetlocation import PresetLocation
|
||||
from .base import Base
|
||||
from .frontline import FrontLine
|
||||
from .missiontarget import MissionTarget
|
||||
@@ -106,48 +107,53 @@ class PresetLocations:
|
||||
|
||||
#: Locations used by non-carrier ships that will be spawned unless the faction has
|
||||
#: no navy or the player has disabled ship generation for the owning side.
|
||||
ships: List[PointWithHeading] = field(default_factory=list)
|
||||
ships: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
#: Locations used by coastal defenses that are generated if the faction is capable.
|
||||
coastal_defenses: List[PointWithHeading] = field(default_factory=list)
|
||||
coastal_defenses: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
#: Locations used by ground based strike objectives.
|
||||
strike_locations: List[PointWithHeading] = field(default_factory=list)
|
||||
strike_locations: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
#: Locations used by offshore strike objectives.
|
||||
offshore_strike_locations: List[PointWithHeading] = field(default_factory=list)
|
||||
offshore_strike_locations: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
#: Locations used by missile sites like scuds and V-2s that are generated if the
|
||||
#: faction is capable.
|
||||
missile_sites: List[PointWithHeading] = field(default_factory=list)
|
||||
missile_sites: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
#: Locations of long range SAMs.
|
||||
long_range_sams: List[PointWithHeading] = field(default_factory=list)
|
||||
long_range_sams: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
#: Locations of medium range SAMs.
|
||||
medium_range_sams: List[PointWithHeading] = field(default_factory=list)
|
||||
medium_range_sams: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
#: Locations of short range SAMs.
|
||||
short_range_sams: List[PointWithHeading] = field(default_factory=list)
|
||||
short_range_sams: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
#: Locations of AAA groups.
|
||||
aaa: List[PointWithHeading] = field(default_factory=list)
|
||||
aaa: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
#: Locations of EWRs.
|
||||
ewrs: List[PointWithHeading] = field(default_factory=list)
|
||||
ewrs: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
#: Locations of map scenery to create zones for.
|
||||
scenery: List[SceneryGroup] = field(default_factory=list)
|
||||
|
||||
#: Locations of factories for producing ground units.
|
||||
factories: List[PointWithHeading] = field(default_factory=list)
|
||||
factories: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
#: Locations of ammo depots for controlling number of units on the front line at a
|
||||
#: control point.
|
||||
ammunition_depots: List[PointWithHeading] = field(default_factory=list)
|
||||
ammunition_depots: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
#: Locations of stationary armor groups.
|
||||
armor_groups: List[PointWithHeading] = field(default_factory=list)
|
||||
armor_groups: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
#: Locations of skynet specific groups
|
||||
iads_connection_node: List[PresetLocation] = field(default_factory=list)
|
||||
iads_power_source: List[PresetLocation] = field(default_factory=list)
|
||||
iads_command_center: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
254
game/theater/iadsnetwork/iadsnetwork.py
Normal file
254
game/theater/iadsnetwork/iadsnetwork.py
Normal file
@@ -0,0 +1,254 @@
|
||||
from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Iterator, Optional
|
||||
from uuid import UUID
|
||||
import uuid
|
||||
from game.theater.iadsnetwork.iadsrole import IadsRole
|
||||
|
||||
from game.theater.theatergroundobject import (
|
||||
IadsBuildingGroundObject,
|
||||
IadsGroundObject,
|
||||
NavalGroundObject,
|
||||
TheaterGroundObject,
|
||||
)
|
||||
from game.theater.theatergroup import IadsGroundGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
|
||||
|
||||
class IadsNetworkException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class SkynetNode:
|
||||
"""Dataclass for a SkynetNode used in the LUA Data table by the luagenerator"""
|
||||
|
||||
dcs_name: str
|
||||
player: bool
|
||||
iads_role: IadsRole
|
||||
connections: dict[str, list[str]] = field(default_factory=lambda: defaultdict(list))
|
||||
|
||||
@staticmethod
|
||||
def dcs_name_for_group(group: IadsGroundGroup) -> str:
|
||||
if group.iads_role in [
|
||||
IadsRole.EWR,
|
||||
IadsRole.COMMAND_CENTER,
|
||||
IadsRole.CONNECTION_NODE,
|
||||
IadsRole.POWER_SOURCE,
|
||||
]:
|
||||
# Use UnitName for EWR, CommandCenter, Comms, Power
|
||||
return group.units[0].unit_name
|
||||
else:
|
||||
# Use the GroupName for SAMs, SAMAsEWR and PDs
|
||||
return group.group_name
|
||||
|
||||
@classmethod
|
||||
def from_group(cls, group: IadsGroundGroup) -> SkynetNode:
|
||||
return cls(
|
||||
cls.dcs_name_for_group(group),
|
||||
group.ground_object.is_friendly(True),
|
||||
group.iads_role,
|
||||
)
|
||||
|
||||
|
||||
class IadsNetworkNode:
|
||||
"""IadsNetworkNode which particicpates to the IADS Network and has connections to Power Sources, Comms or Point Defenses. A network node can be a SAM System, EWR or Command Center"""
|
||||
|
||||
def __init__(self, group: IadsGroundGroup) -> None:
|
||||
self.group = group
|
||||
self.connections: dict[UUID, IadsGroundGroup] = {}
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.group.group_name
|
||||
|
||||
def add_connection_for_tgo(self, tgo: TheaterGroundObject) -> None:
|
||||
"""Add all possible connections for the given TGO to the node"""
|
||||
for group in tgo.groups:
|
||||
if isinstance(group, IadsGroundGroup) and group.iads_role.participate:
|
||||
self.add_connection_for_group(group)
|
||||
|
||||
def add_connection_for_group(self, group: IadsGroundGroup) -> None:
|
||||
"""Add connection for the given GroundGroup with unique ID"""
|
||||
self.connections[uuid.uuid4()] = group
|
||||
|
||||
|
||||
class IadsNetwork:
|
||||
"""IADS Network consisting of multiple Network nodes and connections. The Network represents all possible connections of ground objects regardless if a tgo is under control of red or blue. The network can run in either advanced or basic mode. The advanced network can be created by a given configuration in the campaign yaml or computed by Range. The basic mode is a fallback mode which does not use Comms, Power or Command Centers. The network will be used to visualize all connections at the map and for creating the needed Lua data for the skynet plugin"""
|
||||
|
||||
def __init__(
|
||||
self, advanced: bool, iads_data: list[str | dict[str, list[str]]]
|
||||
) -> None:
|
||||
self.advanced_iads = advanced
|
||||
self.ground_objects: dict[str, TheaterGroundObject] = {}
|
||||
self.nodes: list[IadsNetworkNode] = []
|
||||
self.iads_config: dict[str, list[str]] = defaultdict(list)
|
||||
|
||||
# Load Iads config from the campaign data
|
||||
for element in iads_data:
|
||||
if isinstance(element, str):
|
||||
self.iads_config[element] = []
|
||||
elif isinstance(element, dict):
|
||||
for iads_node, iads_connections in element.items():
|
||||
self.iads_config[iads_node] = iads_connections
|
||||
else:
|
||||
raise RuntimeError("Invalid iads_config in campaign")
|
||||
|
||||
def skynet_nodes(self, game: Game) -> list[SkynetNode]:
|
||||
"""Get all skynet nodes from the IADS Network"""
|
||||
skynet_nodes: list[SkynetNode] = []
|
||||
for node in self.nodes:
|
||||
if game.iads_considerate_culling(node.group.ground_object) or (
|
||||
node.group.units[0].is_vehicle and not node.group.units[0].alive
|
||||
):
|
||||
# Skip
|
||||
continue
|
||||
skynet_node = SkynetNode.from_group(node.group)
|
||||
for connection in node.connections.values():
|
||||
if (
|
||||
connection.ground_object.is_friendly(skynet_node.player)
|
||||
and not game.iads_considerate_culling(connection.ground_object)
|
||||
and not (
|
||||
connection.units[0].is_vehicle and not connection.units[0].alive
|
||||
)
|
||||
):
|
||||
skynet_node.connections[connection.iads_role.value].append(
|
||||
SkynetNode.dcs_name_for_group(connection)
|
||||
)
|
||||
skynet_nodes.append(skynet_node)
|
||||
return skynet_nodes
|
||||
|
||||
def update_tgo(self, tgo: TheaterGroundObject) -> None:
|
||||
"""Update the IADS Network for the given TGO"""
|
||||
# Remove existing nodes for the given tgo
|
||||
for cn in self.nodes:
|
||||
if cn.group.ground_object == tgo:
|
||||
self.nodes.remove(cn)
|
||||
try:
|
||||
# Create a new node for the tgo
|
||||
self.node_for_tgo(tgo)
|
||||
# TODO Add the connections or calculate them..
|
||||
except IadsNetworkException:
|
||||
# Not participating
|
||||
pass
|
||||
|
||||
def node_for_group(self, group: IadsGroundGroup) -> IadsNetworkNode:
|
||||
"""Get existing node from the iads network or create a new node"""
|
||||
for cn in self.nodes:
|
||||
if cn.group == group:
|
||||
return cn
|
||||
|
||||
node = IadsNetworkNode(group)
|
||||
self.nodes.append(node)
|
||||
return node
|
||||
|
||||
def node_for_tgo(self, tgo: TheaterGroundObject) -> IadsNetworkNode:
|
||||
"""Get existing node from the iads network or create a new node"""
|
||||
for cn in self.nodes:
|
||||
if cn.group.ground_object == tgo:
|
||||
return cn
|
||||
|
||||
# Create new connection_node if none exists
|
||||
node: Optional[IadsNetworkNode] = None
|
||||
for group in tgo.groups:
|
||||
# TODO Cleanup
|
||||
if isinstance(group, IadsGroundGroup):
|
||||
# The first IadsGroundGroup is always the primary Group
|
||||
if not node and group.iads_role.participate:
|
||||
# Primary Node
|
||||
node = self.node_for_group(group)
|
||||
elif node and group.iads_role == IadsRole.POINT_DEFENSE:
|
||||
# Point Defense Node for this TGO
|
||||
node.add_connection_for_group(group)
|
||||
|
||||
if node is None:
|
||||
# Raise exception as TGO does not participate to the IADS
|
||||
raise IadsNetworkException(f"TGO {tgo.name} not participating to IADS")
|
||||
return node
|
||||
|
||||
def initialize_network(self, ground_objects: Iterator[TheaterGroundObject]) -> None:
|
||||
"""Initialize the IADS network in advanced or basic mode depending on the campaign"""
|
||||
for tgo in ground_objects:
|
||||
self.ground_objects[tgo.original_name] = tgo
|
||||
if self.advanced_iads:
|
||||
# Advanced mode
|
||||
if self.iads_config:
|
||||
# Load from Configuration File
|
||||
self.initialize_network_from_config()
|
||||
else:
|
||||
# Load from Range
|
||||
self.initialize_network_from_range()
|
||||
|
||||
# basic mode if no advanced iads support or network init created no connections
|
||||
if not self.nodes:
|
||||
self.initialize_basic_iads()
|
||||
|
||||
def initialize_basic_iads(self) -> None:
|
||||
"""Initialize the IADS Network in basic mode (SAM & EWR only)"""
|
||||
for go in self.ground_objects.values():
|
||||
if isinstance(go, IadsGroundObject):
|
||||
try:
|
||||
self.node_for_tgo(go)
|
||||
except IadsNetworkException:
|
||||
# TGO does not participate to the IADS -> Skip
|
||||
pass
|
||||
|
||||
def initialize_network_from_config(self) -> None:
|
||||
"""Initialize the IADS Network from a configuration"""
|
||||
for element_name, connections in self.iads_config.items():
|
||||
try:
|
||||
node = self.node_for_tgo(self.ground_objects[element_name])
|
||||
except (KeyError, IadsNetworkException):
|
||||
# Log a warning as this can be normal. Possible case is for example
|
||||
# when the campaign request a Long Range SAM but the faction has none
|
||||
# available. Therefore the TGO will not get populated at all
|
||||
logging.warning(
|
||||
f"IADS: No ground object found for {element_name}. This can be normal behaviour."
|
||||
)
|
||||
continue
|
||||
|
||||
# Find all connected ground_objects
|
||||
for node_name in connections:
|
||||
try:
|
||||
node.add_connection_for_tgo(self.ground_objects[node_name])
|
||||
except (KeyError):
|
||||
logging.error(
|
||||
f"IADS: No ground object found for connection {node_name}"
|
||||
)
|
||||
continue
|
||||
|
||||
def initialize_network_from_range(self) -> None:
|
||||
"""Initialize the IADS Network by range"""
|
||||
for go in self.ground_objects.values():
|
||||
if (
|
||||
isinstance(go, IadsGroundObject)
|
||||
or isinstance(go, NavalGroundObject)
|
||||
or (
|
||||
isinstance(go, IadsBuildingGroundObject)
|
||||
and IadsRole.for_category(go.category) == IadsRole.COMMAND_CENTER
|
||||
)
|
||||
):
|
||||
try:
|
||||
# Set as primary node
|
||||
node = self.node_for_tgo(go)
|
||||
except IadsNetworkException:
|
||||
# TGO does not participate to iads network
|
||||
continue
|
||||
# Find nearby Power or Connection
|
||||
for nearby_go in self.ground_objects.values():
|
||||
if nearby_go == go:
|
||||
continue
|
||||
if (
|
||||
IadsRole.for_category(go.category)
|
||||
in [
|
||||
IadsRole.POWER_SOURCE,
|
||||
IadsRole.CONNECTION_NODE,
|
||||
]
|
||||
and nearby_go.position.distance_to_point(go.position)
|
||||
<= node.group.iads_role.connection_range.meters
|
||||
):
|
||||
node.add_connection_for_tgo(nearby_go)
|
||||
81
game/theater/iadsnetwork/iadsrole.py
Normal file
81
game/theater/iadsnetwork/iadsrole.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from game.data.groups import GroupTask
|
||||
|
||||
from game.utils import Distance
|
||||
|
||||
|
||||
class IadsRole(Enum):
|
||||
#: A radar SAM that should be controlled by Skynet.
|
||||
SAM = "Sam"
|
||||
|
||||
#: A radar SAM that should be controlled and used as an EWR by Skynet.
|
||||
SAM_AS_EWR = "SamAsEwr"
|
||||
|
||||
#: An air defense unit that should be used as point defense by Skynet.
|
||||
POINT_DEFENSE = "PD"
|
||||
|
||||
#: An ewr unit that should provide information to the Skynet IADS.
|
||||
EWR = "Ewr"
|
||||
|
||||
#: IADS Elements which allow the advanced functions of Skynet.
|
||||
CONNECTION_NODE = "ConnectionNode"
|
||||
POWER_SOURCE = "PowerSource"
|
||||
COMMAND_CENTER = "CommandCenter"
|
||||
|
||||
#: All other types of groups that might be present in a SAM TGO. This includes
|
||||
#: SHORADS, AAA, supply trucks, etc. Anything that shouldn't be controlled by Skynet
|
||||
#: should use this role.
|
||||
NO_BEHAVIOR = "NoBehavior"
|
||||
|
||||
@classmethod
|
||||
def for_task(cls, task: GroupTask) -> IadsRole:
|
||||
if task == GroupTask.COMMS:
|
||||
return cls.CONNECTION_NODE
|
||||
elif task == GroupTask.POWER:
|
||||
return cls.POWER_SOURCE
|
||||
elif task == GroupTask.COMMAND_CENTER:
|
||||
return cls.COMMAND_CENTER
|
||||
elif task == GroupTask.POINT_DEFENSE:
|
||||
return cls.POINT_DEFENSE
|
||||
elif task == GroupTask.LORAD:
|
||||
return cls.SAM_AS_EWR
|
||||
elif task == GroupTask.MERAD:
|
||||
return cls.SAM
|
||||
elif task in [
|
||||
GroupTask.EARLY_WARNING_RADAR,
|
||||
GroupTask.NAVY,
|
||||
GroupTask.AIRCRAFT_CARRIER,
|
||||
GroupTask.HELICOPTER_CARRIER,
|
||||
]:
|
||||
return cls.EWR
|
||||
return cls.NO_BEHAVIOR
|
||||
|
||||
@classmethod
|
||||
def for_category(cls, category: str) -> IadsRole:
|
||||
if category == "comms":
|
||||
return cls.CONNECTION_NODE
|
||||
elif category == "power":
|
||||
return cls.POWER_SOURCE
|
||||
elif category == "commandcenter":
|
||||
return cls.COMMAND_CENTER
|
||||
return cls.NO_BEHAVIOR
|
||||
|
||||
@property
|
||||
def connection_range(self) -> Distance:
|
||||
if self == IadsRole.CONNECTION_NODE:
|
||||
return Distance(27780) # 15nm
|
||||
elif self == IadsRole.POWER_SOURCE:
|
||||
return Distance(64820) # 35nm
|
||||
return Distance(0)
|
||||
|
||||
@property
|
||||
def participate(self) -> bool:
|
||||
# Returns true if the Role participates in the skynet
|
||||
# This will exclude NoBehaviour and PD for the time beeing
|
||||
return self not in [
|
||||
IadsRole.NO_BEHAVIOR,
|
||||
IadsRole.POINT_DEFENSE,
|
||||
]
|
||||
34
game/theater/presetlocation.py
Normal file
34
game/theater/presetlocation.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypeVar
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unitgroup import StaticGroup, ShipGroup, VehicleGroup
|
||||
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from game.utils import Heading
|
||||
|
||||
GroupT = TypeVar("GroupT", StaticGroup, ShipGroup, VehicleGroup)
|
||||
|
||||
|
||||
class PresetLocation(PointWithHeading):
|
||||
"""Store information about the Preset Location set by the campaign designer"""
|
||||
|
||||
# This allows to store original name and force a specific type or template
|
||||
original_name: str # Store the original name from the campaign miz
|
||||
|
||||
def __init__(
|
||||
self, name: str, position: Point, heading: Heading = Heading.from_degrees(0)
|
||||
) -> None:
|
||||
super().__init__(position.x, position.y, heading, position._terrain)
|
||||
self.original_name = name
|
||||
|
||||
@classmethod
|
||||
def from_group(cls, group: GroupT) -> PresetLocation:
|
||||
"""Creates a PresetLocation from a placeholder group in the campaign miz"""
|
||||
preset = PresetLocation(
|
||||
group.name,
|
||||
group.position,
|
||||
Heading.from_degrees(group.units[0].heading),
|
||||
)
|
||||
return preset
|
||||
@@ -12,11 +12,13 @@ from game import Game
|
||||
from game.factions.faction import Faction
|
||||
from game.naming import namegen
|
||||
from game.scenery_group import SceneryGroup
|
||||
from game.theater import PointWithHeading
|
||||
from game.theater import PointWithHeading, PresetLocation
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
IadsBuildingGroundObject,
|
||||
)
|
||||
from game.utils import Heading
|
||||
from .theatergroup import SceneryUnit, TheaterGroup, IadsGroundGroup, IadsRole
|
||||
from game.utils import Heading, escape_string_for_lua
|
||||
from game.version import VERSION
|
||||
from . import (
|
||||
ConflictTheater,
|
||||
@@ -25,7 +27,10 @@ from . import (
|
||||
Fob,
|
||||
OffMapSpawn,
|
||||
)
|
||||
from .theatergroup import SceneryUnit, TheaterGroup
|
||||
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
|
||||
from ..data.building_data import IADS_BUILDINGS
|
||||
from ..data.groups import GroupTask
|
||||
from ..armedforces.forcegroup import ForceGroup
|
||||
from ..armedforces.armedforces import ArmedForces
|
||||
from ..armedforces.forcegroup import ForceGroup
|
||||
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
|
||||
@@ -40,6 +45,7 @@ class GeneratorSettings:
|
||||
player_budget: int
|
||||
enemy_budget: int
|
||||
inverted: bool
|
||||
advanced_iads: bool
|
||||
no_carrier: bool
|
||||
no_lha: bool
|
||||
no_player_navy: bool
|
||||
@@ -154,11 +160,11 @@ class ControlPointGroundObjectGenerator:
|
||||
return True
|
||||
|
||||
def generate_ground_object_from_group(
|
||||
self, unit_group: ForceGroup, position: PointWithHeading
|
||||
self, unit_group: ForceGroup, location: PresetLocation
|
||||
) -> None:
|
||||
ground_object = unit_group.generate(
|
||||
namegen.random_objective_name(),
|
||||
position,
|
||||
location,
|
||||
self.control_point,
|
||||
self.game,
|
||||
)
|
||||
@@ -203,8 +209,10 @@ class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
return False
|
||||
self.generate_ground_object_from_group(
|
||||
unit_group,
|
||||
PointWithHeading.from_point(
|
||||
self.control_point.position, self.control_point.heading
|
||||
PresetLocation(
|
||||
self.control_point.name,
|
||||
self.control_point.position,
|
||||
self.control_point.heading,
|
||||
),
|
||||
)
|
||||
self.control_point.name = random.choice(carrier_names)
|
||||
@@ -232,8 +240,10 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
return False
|
||||
self.generate_ground_object_from_group(
|
||||
unit_group,
|
||||
PointWithHeading.from_point(
|
||||
self.control_point.position, self.control_point.heading
|
||||
PresetLocation(
|
||||
self.control_point.name,
|
||||
self.control_point.position,
|
||||
self.control_point.heading,
|
||||
),
|
||||
)
|
||||
self.control_point.name = random.choice(lha_names)
|
||||
@@ -259,8 +269,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
def generate_ground_points(self) -> None:
|
||||
"""Generate ground objects and AA sites for the control point."""
|
||||
self.generate_armor_groups()
|
||||
self.generate_aa()
|
||||
self.generate_ewrs()
|
||||
self.generate_iads()
|
||||
self.generate_scenery_sites()
|
||||
self.generate_strike_targets()
|
||||
self.generate_offshore_strike_targets()
|
||||
@@ -305,7 +314,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
def generate_building_at(
|
||||
self,
|
||||
group_task: GroupTask,
|
||||
position: PointWithHeading,
|
||||
location: PresetLocation,
|
||||
) -> None:
|
||||
# GroupTask is the type of the building to be generated
|
||||
unit_group = self.armed_forces.random_group_for_task(group_task)
|
||||
@@ -313,7 +322,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
raise RuntimeError(
|
||||
f"{self.faction_name} has no access to Building {group_task.description}"
|
||||
)
|
||||
self.generate_ground_object_from_group(unit_group, position)
|
||||
self.generate_ground_object_from_group(unit_group, location)
|
||||
|
||||
def generate_ammunition_depots(self) -> None:
|
||||
for position in self.control_point.preset_locations.ammunition_depots:
|
||||
@@ -323,21 +332,32 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
for position in self.control_point.preset_locations.factories:
|
||||
self.generate_building_at(GroupTask.FACTORY, position)
|
||||
|
||||
def generate_aa_at(
|
||||
self, position: PointWithHeading, tasks: list[GroupTask]
|
||||
) -> None:
|
||||
def generate_aa_at(self, location: PresetLocation, tasks: list[GroupTask]) -> None:
|
||||
for task in tasks:
|
||||
unit_group = self.armed_forces.random_group_for_task(task)
|
||||
if unit_group:
|
||||
# Only take next (smaller) aa_range when no template available for the
|
||||
# most requested range. Otherwise break the loop and continue
|
||||
self.generate_ground_object_from_group(unit_group, position)
|
||||
self.generate_ground_object_from_group(unit_group, location)
|
||||
return
|
||||
|
||||
logging.error(
|
||||
f"{self.faction_name} has no access to SAM {', '.join([task.description for task in tasks])}"
|
||||
)
|
||||
|
||||
def generate_iads(self) -> None:
|
||||
# AntiAir
|
||||
self.generate_aa()
|
||||
# EWR
|
||||
self.generate_ewrs()
|
||||
# IADS Buildings
|
||||
for iads_element in self.control_point.preset_locations.iads_command_center:
|
||||
self.generate_building_at(GroupTask.COMMAND_CENTER, iads_element)
|
||||
for iads_element in self.control_point.preset_locations.iads_connection_node:
|
||||
self.generate_building_at(GroupTask.COMMS, iads_element)
|
||||
for iads_element in self.control_point.preset_locations.iads_power_source:
|
||||
self.generate_building_at(GroupTask.POWER, iads_element)
|
||||
|
||||
def generate_scenery_sites(self) -> None:
|
||||
presets = self.control_point.preset_locations
|
||||
for scenery_group in presets.scenery:
|
||||
@@ -345,11 +365,14 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
|
||||
def generate_tgo_for_scenery(self, scenery: SceneryGroup) -> None:
|
||||
# Special Handling for scenery Objects based on trigger zones
|
||||
g = BuildingGroundObject(
|
||||
iads_role = IadsRole.for_category(scenery.category)
|
||||
tgo_type = (
|
||||
IadsBuildingGroundObject if iads_role.participate else BuildingGroundObject
|
||||
)
|
||||
g = tgo_type(
|
||||
namegen.random_objective_name(),
|
||||
scenery.category,
|
||||
scenery.position,
|
||||
Heading.from_degrees(0),
|
||||
PresetLocation(scenery.zone_def.name, scenery.position),
|
||||
self.control_point,
|
||||
)
|
||||
ground_group = TheaterGroup(
|
||||
@@ -359,9 +382,14 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
[],
|
||||
g,
|
||||
)
|
||||
if iads_role.participate:
|
||||
ground_group = IadsGroundGroup.from_group(ground_group)
|
||||
ground_group.iads_role = iads_role
|
||||
|
||||
g.groups.append(ground_group)
|
||||
# Each nested trigger zone is a target/building/unit for an objective.
|
||||
for zone in scenery.zones:
|
||||
zone.name = escape_string_for_lua(zone.name)
|
||||
scenery_unit = SceneryUnit(
|
||||
zone.id,
|
||||
zone.name,
|
||||
@@ -409,8 +437,10 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
||||
def generate_fob(self) -> None:
|
||||
self.generate_building_at(
|
||||
GroupTask.FOB,
|
||||
PointWithHeading.from_point(
|
||||
self.control_point.position, self.control_point.heading
|
||||
PresetLocation(
|
||||
self.control_point.name,
|
||||
self.control_point.position,
|
||||
self.control_point.heading,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ from game.sidc import (
|
||||
Status,
|
||||
SymbolSet,
|
||||
)
|
||||
from game.theater.presetlocation import PresetLocation
|
||||
from .missiontarget import MissionTarget
|
||||
from ..data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS
|
||||
from ..utils import Distance, Heading, meters
|
||||
@@ -41,6 +42,7 @@ NAME_BY_CATEGORY = {
|
||||
"ammo": "Ammo depot",
|
||||
"armor": "Armor group",
|
||||
"coastal": "Coastal defense",
|
||||
"commandcenter": "Command Center",
|
||||
"comms": "Communications tower",
|
||||
"derrick": "Derrick",
|
||||
"factory": "Factory",
|
||||
@@ -62,18 +64,18 @@ class TheaterGroundObject(MissionTarget, SidcDescribable, ABC):
|
||||
self,
|
||||
name: str,
|
||||
category: str,
|
||||
position: Point,
|
||||
heading: Heading,
|
||||
location: PresetLocation,
|
||||
control_point: ControlPoint,
|
||||
sea_object: bool,
|
||||
) -> None:
|
||||
super().__init__(name, position)
|
||||
super().__init__(name, location)
|
||||
self.id = uuid.uuid4()
|
||||
self.category = category
|
||||
self.heading = heading
|
||||
self.heading = location.heading
|
||||
self.control_point = control_point
|
||||
self.sea_object = sea_object
|
||||
self.groups: List[TheaterGroup] = []
|
||||
self.original_name = location.original_name
|
||||
self._threat_poly: ThreatPoly | None = None
|
||||
|
||||
def __getstate__(self) -> dict[str, Any]:
|
||||
@@ -127,6 +129,11 @@ class TheaterGroundObject(MissionTarget, SidcDescribable, ABC):
|
||||
"""The name of the unit group."""
|
||||
return f"{self.category}|{self.name}"
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
"""The display name of the tgo which will be shown on the map."""
|
||||
return self.group_name
|
||||
|
||||
@property
|
||||
def waypoint_name(self) -> str:
|
||||
return f"[{self.name}] {self.category}"
|
||||
@@ -290,16 +297,14 @@ class BuildingGroundObject(TheaterGroundObject):
|
||||
self,
|
||||
name: str,
|
||||
category: str,
|
||||
position: Point,
|
||||
heading: Heading,
|
||||
location: PresetLocation,
|
||||
control_point: ControlPoint,
|
||||
is_fob_structure: bool = False,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category=category,
|
||||
position=position,
|
||||
heading=heading,
|
||||
location=location,
|
||||
control_point=control_point,
|
||||
sea_object=False,
|
||||
)
|
||||
@@ -311,6 +316,8 @@ class BuildingGroundObject(TheaterGroundObject):
|
||||
entity = LandInstallationEntity.TENTED_CAMP
|
||||
elif self.category == "ammo":
|
||||
entity = LandInstallationEntity.AMMUNITION_CACHE
|
||||
elif self.category == "commandcenter":
|
||||
entity = LandInstallationEntity.MILITARY_INFRASTRUCTURE
|
||||
elif self.category == "comms":
|
||||
entity = LandInstallationEntity.TELECOMMUNICATIONS_TOWER
|
||||
elif self.category == "derrick":
|
||||
@@ -389,12 +396,13 @@ class GenericCarrierGroundObject(NavalGroundObject, ABC):
|
||||
|
||||
# TODO: Why is this both a CP and a TGO?
|
||||
class CarrierGroundObject(GenericCarrierGroundObject):
|
||||
def __init__(self, name: str, control_point: ControlPoint) -> None:
|
||||
def __init__(
|
||||
self, name: str, location: PresetLocation, control_point: ControlPoint
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="CARRIER",
|
||||
position=control_point.position,
|
||||
heading=Heading.from_degrees(0),
|
||||
location=location,
|
||||
control_point=control_point,
|
||||
sea_object=True,
|
||||
)
|
||||
@@ -403,24 +411,19 @@ class CarrierGroundObject(GenericCarrierGroundObject):
|
||||
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
|
||||
return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.CARRIER
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
# Prefix the group names with the side color so Skynet can find them,
|
||||
# add to EWR.
|
||||
return f"{self.faction_color}|EWR|{super().group_name}"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"CV {self.name}"
|
||||
|
||||
|
||||
# TODO: Why is this both a CP and a TGO?
|
||||
class LhaGroundObject(GenericCarrierGroundObject):
|
||||
def __init__(self, name: str, control_point: ControlPoint) -> None:
|
||||
def __init__(
|
||||
self, name: str, location: PresetLocation, control_point: ControlPoint
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="LHA",
|
||||
position=control_point.position,
|
||||
heading=Heading.from_degrees(0),
|
||||
location=location,
|
||||
control_point=control_point,
|
||||
sea_object=True,
|
||||
)
|
||||
@@ -429,25 +432,18 @@ class LhaGroundObject(GenericCarrierGroundObject):
|
||||
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
|
||||
return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.AMPHIBIOUS_ASSAULT_SHIP_GENERAL
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
# Prefix the group names with the side color so Skynet can find them,
|
||||
# add to EWR.
|
||||
return f"{self.faction_color}|EWR|{super().group_name}"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"LHA {self.name}"
|
||||
|
||||
|
||||
class MissileSiteGroundObject(TheaterGroundObject):
|
||||
def __init__(
|
||||
self, name: str, position: Point, heading: Heading, control_point: ControlPoint
|
||||
self, name: str, location: PresetLocation, control_point: ControlPoint
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="missile",
|
||||
position=position,
|
||||
heading=heading,
|
||||
location=location,
|
||||
control_point=control_point,
|
||||
sea_object=False,
|
||||
)
|
||||
@@ -469,15 +465,13 @@ class CoastalSiteGroundObject(TheaterGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
position: Point,
|
||||
location: PresetLocation,
|
||||
control_point: ControlPoint,
|
||||
heading: Heading,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="coastal",
|
||||
position=position,
|
||||
heading=heading,
|
||||
location=location,
|
||||
control_point=control_point,
|
||||
sea_object=False,
|
||||
)
|
||||
@@ -496,6 +490,21 @@ class CoastalSiteGroundObject(TheaterGroundObject):
|
||||
|
||||
|
||||
class IadsGroundObject(TheaterGroundObject, ABC):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
location: PresetLocation,
|
||||
control_point: ControlPoint,
|
||||
category: str = "aa",
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category=category,
|
||||
location=location,
|
||||
control_point=control_point,
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from game.ato import FlightType
|
||||
|
||||
@@ -511,17 +520,14 @@ class SamGroundObject(IadsGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
position: Point,
|
||||
heading: Heading,
|
||||
location: PresetLocation,
|
||||
control_point: ControlPoint,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="aa",
|
||||
position=position,
|
||||
heading=heading,
|
||||
location=location,
|
||||
control_point=control_point,
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -591,15 +597,13 @@ class VehicleGroupGroundObject(TheaterGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
position: Point,
|
||||
heading: Heading,
|
||||
location: PresetLocation,
|
||||
control_point: ControlPoint,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="armor",
|
||||
position=position,
|
||||
heading=heading,
|
||||
location=location,
|
||||
control_point=control_point,
|
||||
sea_object=False,
|
||||
)
|
||||
@@ -624,29 +628,20 @@ class EwrGroundObject(IadsGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
position: Point,
|
||||
heading: Heading,
|
||||
location: PresetLocation,
|
||||
control_point: ControlPoint,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="ewr",
|
||||
position=position,
|
||||
heading=heading,
|
||||
location=location,
|
||||
control_point=control_point,
|
||||
sea_object=False,
|
||||
category="ewr",
|
||||
)
|
||||
|
||||
@property
|
||||
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
|
||||
return SymbolSet.LAND_EQUIPMENT, LandEquipmentEntity.RADAR
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
# Prefix the group names with the side color so Skynet can find them.
|
||||
# Use Group Id and uppercase EWR
|
||||
return f"{self.faction_color}|EWR|{self.name}"
|
||||
|
||||
@property
|
||||
def might_have_aa(self) -> bool:
|
||||
return True
|
||||
@@ -661,12 +656,13 @@ class EwrGroundObject(IadsGroundObject):
|
||||
|
||||
|
||||
class ShipGroundObject(NavalGroundObject):
|
||||
def __init__(self, name: str, position: Point, control_point: ControlPoint) -> None:
|
||||
def __init__(
|
||||
self, name: str, location: PresetLocation, control_point: ControlPoint
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="ship",
|
||||
position=position,
|
||||
heading=Heading.from_degrees(0),
|
||||
location=location,
|
||||
control_point=control_point,
|
||||
sea_object=True,
|
||||
)
|
||||
@@ -675,8 +671,10 @@ class ShipGroundObject(NavalGroundObject):
|
||||
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
|
||||
return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.SURFACE_COMBATANT_LINE
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
# Prefix the group names with the side color so Skynet can find them,
|
||||
# add to EWR.
|
||||
return f"{self.faction_color}|EWR|{super().group_name}"
|
||||
|
||||
class IadsBuildingGroundObject(BuildingGroundObject):
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from game.ato import FlightType
|
||||
|
||||
if not self.is_friendly(for_player):
|
||||
yield from [FlightType.STRIKE, FlightType.DEAD]
|
||||
|
||||
@@ -2,15 +2,18 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, TYPE_CHECKING, Type
|
||||
from enum import Enum
|
||||
|
||||
from dcs.triggers import TriggerZone
|
||||
from dcs.unittype import ShipType, StaticType, UnitType as DcsUnitType, VehicleType
|
||||
|
||||
from game.data.groups import GroupTask
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.dcs.shipunittype import ShipUnitType
|
||||
from game.dcs.unittype import UnitType
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from game.utils import Heading
|
||||
from game.theater.iadsnetwork.iadsrole import IadsRole
|
||||
from game.utils import Heading, Distance
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.layout.layout import LayoutUnit
|
||||
@@ -144,8 +147,6 @@ class TheaterGroup:
|
||||
name: str,
|
||||
units: list[TheaterUnit],
|
||||
go: TheaterGroundObject,
|
||||
unit_type: Type[DcsUnitType],
|
||||
unit_count: int,
|
||||
) -> TheaterGroup:
|
||||
return TheaterGroup(
|
||||
id,
|
||||
@@ -166,3 +167,18 @@ class TheaterGroup:
|
||||
@property
|
||||
def alive_units(self) -> int:
|
||||
return sum([unit.alive for unit in self.units])
|
||||
|
||||
|
||||
class IadsGroundGroup(TheaterGroup):
|
||||
# IADS GroundObject Groups have a specific Role for the system
|
||||
iads_role: IadsRole = IadsRole.NO_BEHAVIOR
|
||||
|
||||
@staticmethod
|
||||
def from_group(group: TheaterGroup) -> IadsGroundGroup:
|
||||
return IadsGroundGroup(
|
||||
group.id,
|
||||
group.name,
|
||||
group.position,
|
||||
group.units,
|
||||
group.ground_object,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Iterable
|
||||
|
||||
Reference in New Issue
Block a user