Refactor Templates to Layouts, Review and Cleanup

- Fix tgogenerator
- Fix UI for ForceGroup and Layouts
- Fix ammo depot handling
- Split bigger files in smaller meaningful files (TGO, layouts, forces)
- Renamed Template to Layout
- Renamed GroundGroup to TheaterGroup and GroundUnit to TheaterUnit
- Reorganize Layouts and UnitGroups to a ArmedForces class and ForceGroup similar to the AirWing and Squadron
- Reworded the UnitClass, GroupRole, GroupTask (adopted to PEP8) and reworked the connection from Role and Task
- added comments
- added missing unit classes
- added temp workaround for missing classes
- add repariable property to TheaterUnit
- Review and Cleanup

Added serialization for loaded templates

Loading the templates from the .miz files takes a lot of computation time and in the future there will be more templates added to the system. Therefore a local pickle serialization for the loaded templates was re-added:
- The pickle will be created the first time the TemplateLoader will be accessed
- Pickle is stored in Liberation SaveDir
- Added UI option to (re-)import templates
This commit is contained in:
RndName
2022-02-10 12:23:16 +01:00
parent 1ae6503ceb
commit 2c17a9a52e
138 changed files with 1985 additions and 3096 deletions

View File

@@ -0,0 +1,82 @@
from __future__ import annotations
import random
from typing import TYPE_CHECKING, Iterator, Optional
from game import db
from game.data.groups import GroupRole, GroupTask
from game.armedforces.forcegroup import ForceGroup
from game.profiling import logged_duration
if TYPE_CHECKING:
from game.factions.faction import Faction
# TODO More comments and rename
class ArmedForces:
"""TODO Description"""
# All available force groups for a specific Role
forces: dict[GroupRole, list[ForceGroup]]
def __init__(self, faction: Faction):
with logged_duration(f"Loading armed forces for {faction.name}"):
self._load_forces(faction)
def add_or_update_force_group(self, new_group: ForceGroup) -> None:
"""TODO Description"""
if new_group.role in self.forces:
# Check if a force group with the same units exists
for force_group in self.forces[new_group.role]:
if (
force_group.units == new_group.units
and force_group.tasks == new_group.tasks
):
# Update existing group if units and tasks are equal
force_group.update_group(new_group)
return
# Add a new force group
self.add_force_group(new_group)
def add_force_group(self, force_group: ForceGroup) -> None:
"""Adds a force group to the forces"""
if force_group.role in self.forces:
self.forces[force_group.role].append(force_group)
else:
self.forces[force_group.role] = [force_group]
def _load_forces(self, faction: Faction) -> None:
"""Initialize all armed_forces for the given faction"""
# This function will create a ForgeGroup for each global Layout and PresetGroup
self.forces = {}
preset_layouts = [
layout
for preset_group in faction.preset_groups
for layout in preset_group.layouts
]
# Generate Troops for all generic layouts and presets
for layout in db.LAYOUTS.layouts:
if (
layout.generic or layout in preset_layouts
) and layout.usable_by_faction(faction):
# Creates a faction compatible GorceGroup
self.add_or_update_force_group(ForceGroup.for_layout(layout, faction))
def groups_for_task(self, group_task: GroupTask) -> Iterator[ForceGroup]:
for groups in self.forces.values():
for unit_group in groups:
if group_task in unit_group.tasks:
yield unit_group
def groups_for_tasks(self, tasks: list[GroupTask]) -> list[ForceGroup]:
groups = []
for task in tasks:
for group in self.groups_for_task(task):
if group not in groups:
groups.append(group)
return groups
def random_group_for_task(self, group_task: GroupTask) -> Optional[ForceGroup]:
unit_groups = list(self.groups_for_task(group_task))
return random.choice(unit_groups) if unit_groups else None

View File

@@ -0,0 +1,250 @@
from __future__ import annotations
import logging
import random
from dataclasses import dataclass, field
from pathlib import Path
from typing import ClassVar, TYPE_CHECKING, Type, Any, Iterator, Optional
import yaml
from dcs import Point
from game import db
from game.data.groups import GroupRole, GroupTask
from game.data.radar_db import UNITS_WITH_RADAR
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.layout.layout import TheaterLayout, AntiAirLayout, GroupLayout
from dcs.unittype import UnitType as DcsUnitType, VehicleType, ShipType, StaticType
from game.theater.theatergroup import TheaterGroup
if TYPE_CHECKING:
from game import Game
from game.factions.faction import Faction
from game.theater import TheaterGroundObject, ControlPoint
@dataclass
class ForceGroup:
"""A logical group of multiple units and layouts which have a specific tasking"""
name: str
units: list[UnitType[Any]]
statics: list[Type[DcsUnitType]]
role: GroupRole
tasks: list[GroupTask] = field(default_factory=list)
layouts: list[TheaterLayout] = field(default_factory=list)
_by_name: ClassVar[dict[str, ForceGroup]] = {}
_by_role: ClassVar[dict[GroupRole, list[ForceGroup]]] = {}
_loaded: bool = False
@staticmethod
def for_layout(layout: TheaterLayout, faction: Faction) -> ForceGroup:
"""TODO Documentation"""
units: set[UnitType[Any]] = set()
statics: set[Type[DcsUnitType]] = set()
for group in layout.groups:
for unit_type in group.possible_types_for_faction(faction):
if issubclass(unit_type, VehicleType):
units.add(next(GroundUnitType.for_dcs_type(unit_type)))
elif issubclass(unit_type, ShipType):
units.add(next(ShipUnitType.for_dcs_type(unit_type)))
elif issubclass(unit_type, StaticType):
statics.add(unit_type)
return ForceGroup(
f"{layout.role.value}: {', '.join([t.description for t in layout.tasks])}",
list(units),
list(statics),
layout.role,
layout.tasks,
[layout],
)
def __str__(self) -> str:
return self.name
@classmethod
def named(cls, name: str) -> ForceGroup:
if not cls._loaded:
cls._load_all()
return cls._by_name[name]
def has_access_to_dcs_type(self, type: Type[DcsUnitType]) -> bool:
return (
any(unit.dcs_unit_type == type for unit in self.units)
or type in self.statics
)
def dcs_unit_types_for_group(self, group: GroupLayout) -> list[Type[DcsUnitType]]:
"""TODO Description"""
unit_types = [t for t in group.unit_types if self.has_access_to_dcs_type(t)]
alternative_types = []
for accessible_unit in self.units:
if accessible_unit.unit_class in group.unit_classes:
unit_types.append(accessible_unit.dcs_unit_type)
if accessible_unit.unit_class in group.alternative_classes:
alternative_types.append(accessible_unit.dcs_unit_type)
return unit_types or alternative_types
def unit_types_for_group(self, group: GroupLayout) -> Iterator[UnitType[Any]]:
for dcs_type in self.dcs_unit_types_for_group(group):
if issubclass(dcs_type, VehicleType):
yield next(GroundUnitType.for_dcs_type(dcs_type))
elif issubclass(dcs_type, ShipType):
yield next(ShipUnitType.for_dcs_type(dcs_type))
def statics_for_group(self, group: GroupLayout) -> Iterator[Type[DcsUnitType]]:
for dcs_type in self.dcs_unit_types_for_group(group):
if issubclass(dcs_type, StaticType):
yield dcs_type
def random_dcs_unit_type_for_group(self, group: GroupLayout) -> Type[DcsUnitType]:
"""TODO Description"""
return random.choice(self.dcs_unit_types_for_group(group))
def update_group(self, new_group: ForceGroup) -> None:
"""Update the group from another group. This will merge statics and layouts."""
# Merge layouts and statics
self.statics = list(set(self.statics + new_group.statics))
self.layouts = list(set(self.layouts + new_group.layouts))
def generate(
self,
name: str,
position: PointWithHeading,
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
)
def create_ground_object_for_layout(
self,
layout: TheaterLayout,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
game: Game,
) -> TheaterGroundObject:
"""Create a TheaterGroundObject for the given template"""
go = layout.create_ground_object(name, position, control_point)
# Generate all groups using the randomization if it defined
for group in layout.groups:
# Choose a random unit_type for the group
try:
unit_type = self.random_dcs_unit_type_for_group(group)
except IndexError:
if group.optional:
# If group is optional it is ok when no unit_type is available
continue
# if non-optional this is a error
raise RuntimeError(f"No accessible unit for {self.name} - {group.name}")
self.create_theater_group_for_tgo(go, group, name, game, unit_type)
return go
def create_theater_group_for_tgo(
self,
ground_object: TheaterGroundObject,
group: GroupLayout,
name: str,
game: Game,
unit_type: Type[DcsUnitType],
unit_count: Optional[int] = None,
) -> None:
"""Create a TheaterGroup and add it to the given TGO"""
# Random UnitCounter if not forced
if unit_count is None:
unit_count = group.unit_counter
# Static and non Static groups have to be separated
group_id = group.group - 1
if len(ground_object.groups) <= group_id:
# Requested group was not yet created
ground_group = TheaterGroup.from_template(
game.next_group_id(), group, ground_object, unit_type, unit_count
)
# Set Group Name
ground_group.name = f"{name} {group_id}"
ground_object.groups.append(ground_group)
units = ground_group.units
else:
ground_group = ground_object.groups[group_id]
units = group.generate_units(ground_object, unit_type, unit_count)
ground_group.units.extend(units)
# Assign UniqueID, name and align relative to ground_object
for u_id, unit in enumerate(units):
unit.id = game.next_unit_id()
unit.name = unit.unit_type.name if unit.unit_type else unit.type.name
unit.position = PointWithHeading.from_point(
Point(
ground_object.position.x + unit.position.x,
ground_object.position.y + unit.position.y,
),
# Align heading to GroundObject defined by the campaign designer
unit.position.heading + ground_object.heading,
)
if (
isinstance(self, AntiAirLayout)
and unit.unit_type
and unit.unit_type.dcs_unit_type in UNITS_WITH_RADAR
):
# Head Radars towards the center of the conflict
unit.position.heading = (
game.theater.heading_to_conflict_from(unit.position)
or unit.position.heading
)
# Rotate unit around the center to align the orientation of the group
unit.position.rotate(ground_object.position, ground_object.heading)
@classmethod
def _load_all(cls) -> None:
for file in Path("resources/units/groups").glob("*.yaml"):
if not file.is_file():
raise RuntimeError(f"{file.name} is not a valid ForceGroup")
with file.open(encoding="utf-8") as data_file:
data = yaml.safe_load(data_file)
group_role = GroupRole(data.get("role"))
group_tasks = [GroupTask.by_description(n) for n in data.get("tasks", [])]
units = [UnitType.named(unit) for unit in data.get("units", [])]
statics = []
for static in data.get("statics", []):
static_type = db.static_type_from_name(static)
if static_type is None:
logging.error(f"Static {static} for {file} is not valid")
else:
statics.append(static_type)
layouts = [next(db.LAYOUTS.by_name(n)) for n in data.get("layouts")]
force_group = ForceGroup(
name=data.get("name"),
units=units,
statics=statics,
role=group_role,
tasks=group_tasks,
layouts=layouts,
)
cls._by_name[force_group.name] = force_group
if group_role in cls._by_role:
cls._by_role[group_role].append(force_group)
else:
cls._by_role[group_role] = [force_group]
cls._loaded = True

View File

@@ -9,6 +9,7 @@ from game.campaignloader import CampaignAirWingConfig
from game.campaignloader.defaultsquadronassigner import DefaultSquadronAssigner
from game.commander import TheaterCommander
from game.commander.missionscheduler import MissionScheduler
from game.armedforces.armedforces import ArmedForces
from game.income import Income
from game.navmesh import NavMesh
from game.orderedset import OrderedSet
@@ -41,6 +42,7 @@ class Coalition:
self.bullseye = Bullseye(Point(0, 0))
self.faker = Faker(self.faction.locales)
self.air_wing = AirWing(player, game, self.faction)
self.armed_forces = ArmedForces(self.faction)
self.transfers = PendingTransfers(game, player)
# Late initialized because the two coalitions in the game are mutually

View File

@@ -1,5 +1,7 @@
from dcs.vehicles import AirDefence
from game.theater.theatergroup import TheaterUnit
class AlicCodes:
CODES = {
@@ -37,5 +39,5 @@ class AlicCodes:
}
@classmethod
def code_for(cls, unit_type: str) -> int:
return cls.CODES[unit_type]
def code_for(cls, unit: TheaterUnit) -> int:
return cls.CODES[unit.type.id]

View File

@@ -1,6 +1,12 @@
import inspect
import dcs
REQUIRED_BUILDINGS = [
"ammo",
"factory",
"fob",
]
DEFAULT_AVAILABLE_BUILDINGS = [
"fuel",
"comms",

View File

@@ -104,13 +104,13 @@ MODERN_DOCTRINE = Doctrine(
sweep_distance=nautical_miles(60),
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{
UnitClass.Tank: 3,
UnitClass.Atgm: 2,
UnitClass.Apc: 2,
UnitClass.Ifv: 3,
UnitClass.Artillery: 1,
UnitClass.TANK: 3,
UnitClass.ATGM: 2,
UnitClass.APC: 2,
UnitClass.IFV: 3,
UnitClass.ARTILLERY: 1,
UnitClass.SHORAD: 2,
UnitClass.Recon: 1,
UnitClass.RECON: 1,
}
),
)
@@ -141,13 +141,13 @@ COLDWAR_DOCTRINE = Doctrine(
sweep_distance=nautical_miles(40),
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{
UnitClass.Tank: 4,
UnitClass.Atgm: 2,
UnitClass.Apc: 3,
UnitClass.Ifv: 2,
UnitClass.Artillery: 1,
UnitClass.TANK: 4,
UnitClass.ATGM: 2,
UnitClass.APC: 3,
UnitClass.IFV: 2,
UnitClass.ARTILLERY: 1,
UnitClass.SHORAD: 2,
UnitClass.Recon: 1,
UnitClass.RECON: 1,
}
),
)
@@ -178,12 +178,12 @@ WWII_DOCTRINE = Doctrine(
sweep_distance=nautical_miles(10),
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{
UnitClass.Tank: 3,
UnitClass.Atgm: 3,
UnitClass.Apc: 3,
UnitClass.Artillery: 1,
UnitClass.TANK: 3,
UnitClass.ATGM: 3,
UnitClass.APC: 3,
UnitClass.ARTILLERY: 1,
UnitClass.SHORAD: 3,
UnitClass.Recon: 1,
UnitClass.RECON: 1,
}
),
)

View File

@@ -1,63 +1,69 @@
from __future__ import annotations
from enum import Enum
class GroupRole(Enum):
Unknow = "Unknown"
AntiAir = "AntiAir"
Building = "Building"
Naval = "Naval"
GroundForce = "GroundForce"
Defenses = "Defenses"
Air = "Air"
"""Role of a ForceGroup within the ArmedForces"""
AIR_DEFENSE = "AntiAir"
BUILDING = "Building"
DEFENSES = "Defenses"
GROUND_FORCE = "GroundForce"
NAVAL = "Naval"
@property
def tasks(self) -> list[GroupTask]:
return [task for task in GroupTask if task.role == self]
class GroupTask(Enum):
EWR = "EarlyWarningRadar"
AAA = "AAA"
SHORAD = "SHORAD"
MERAD = "MERAD"
LORAD = "LORAD"
AircraftCarrier = "AircraftCarrier"
HelicopterCarrier = "HelicopterCarrier"
Navy = "Navy"
BaseDefense = "BaseDefense" # Ground
FrontLine = "FrontLine"
Air = "Air"
Missile = "Missile"
Coastal = "Coastal"
Factory = "Factory"
Ammo = "Ammo"
Oil = "Oil"
FOB = "FOB"
StrikeTarget = "StrikeTarget"
Comms = "Comms"
Power = "Power"
"""Specific Tasking of a ForceGroup"""
def __init__(self, description: str, role: GroupRole):
self.description = description
self.role = role
ROLE_TASKINGS: dict[GroupRole, list[GroupTask]] = {
GroupRole.Unknow: [], # No Tasking
GroupRole.AntiAir: [
GroupTask.EWR,
GroupTask.AAA,
GroupTask.SHORAD,
GroupTask.MERAD,
GroupTask.LORAD,
],
GroupRole.GroundForce: [GroupTask.BaseDefense, GroupTask.FrontLine],
GroupRole.Naval: [
GroupTask.AircraftCarrier,
GroupTask.HelicopterCarrier,
GroupTask.Navy,
],
GroupRole.Building: [
GroupTask.Factory,
GroupTask.Ammo,
GroupTask.Oil,
GroupTask.FOB,
GroupTask.StrikeTarget,
GroupTask.Comms,
GroupTask.Power,
],
GroupRole.Defenses: [GroupTask.Missile, GroupTask.Coastal],
GroupRole.Air: [GroupTask.Air],
}
@classmethod
def by_description(cls, description: str) -> GroupTask:
for task in GroupTask:
if task.description == description:
return task
raise RuntimeError(f"GroupTask with description {description} does not exist")
# ANTI AIR
AAA = ("AAA", GroupRole.AIR_DEFENSE)
EARLY_WARNING_RADAR = ("EarlyWarningRadar", GroupRole.AIR_DEFENSE)
LORAD = ("LORAD", GroupRole.AIR_DEFENSE)
MERAD = ("MERAD", GroupRole.AIR_DEFENSE)
SHORAD = ("SHORAD", GroupRole.AIR_DEFENSE)
# NAVAL
AIRCRAFT_CARRIER = ("AircraftCarrier", GroupRole.NAVAL)
HELICOPTER_CARRIER = ("HelicopterCarrier", GroupRole.NAVAL)
NAVY = ("Navy", GroupRole.NAVAL)
# GROUND FORCES
BASE_DEFENSE = ("BaseDefense", GroupRole.GROUND_FORCE)
FRONT_LINE = ("FrontLine", GroupRole.GROUND_FORCE)
# DEFENSES
COASTAL = ("Coastal", GroupRole.DEFENSES)
MISSILE = ("Missile", GroupRole.DEFENSES)
# 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)
FOB = ("FOB", GroupRole.BUILDING)
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)

View File

@@ -2,39 +2,40 @@ from __future__ import annotations
from enum import unique, Enum
from game.data.groups import GroupRole, GroupTask
@unique
class UnitClass(Enum):
Unknown = "Unknown"
Tank = "Tank"
Atgm = "ATGM"
Ifv = "IFV"
Apc = "APC"
Artillery = "Artillery"
Logistics = "Logistics"
Recon = "Recon"
Infantry = "Infantry"
UNKNOWN = "Unknown"
AAA = "AAA"
AIRCRAFT_CARRIER = "AircraftCarrier"
APC = "APC"
ARTILLERY = "Artillery"
ATGM = "ATGM"
BOAT = "Boat"
COMMAND_POST = "CommandPost"
CRUISER = "Cruiser"
DESTROYER = "Destroyer"
EARLY_WARNING_RADAR = "EarlyWarningRadar"
FORTIFICATION = "Fortification"
FRIGATE = "Frigate"
HELICOPTER_CARRIER = "HelicopterCarrier"
IFV = "IFV"
INFANTRY = "Infantry"
LANDING_SHIP = "LandingShip"
LAUNCHER = "Launcher"
LOGISTICS = "Logistics"
MANPAD = "Manpad"
MISSILE = "Missile"
OPTICAL_TRACKER = "OpticalTracker"
PLANE = "Plane"
POWER = "Power"
RECON = "Recon"
SEARCH_LIGHT = "SearchLight"
SEARCH_RADAR = "SearchRadar"
SEARCH_TRACK_RADAR = "SearchTrackRadar"
SHORAD = "SHORAD"
Manpad = "Manpad"
SR = "SearchRadar"
STR = "SearchTrackRadar"
LowAltSR = "LowAltSearchRadar"
TR = "TrackRadar"
LN = "Launcher"
EWR = "EarlyWarningRadar"
SPECIALIZED_RADAR = "SpecializedRadar"
SUBMARINE = "Submarine"
TANK = "Tank"
TELAR = "TELAR"
Missile = "Missile"
AircraftCarrier = "AircraftCarrier"
HelicopterCarrier = "HelicopterCarrier"
Destroyer = "Destroyer"
Cruiser = "Cruiser"
Submarine = "Submarine"
LandingShip = "LandingShip"
Boat = "Boat"
Plane = "Plane"
def to_dict(self) -> str:
return self.value
TRACK_RADAR = "TrackRadar"

View File

@@ -1,11 +1,10 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from functools import cached_property
from pathlib import Path
from typing import Any, ClassVar, Iterator, Optional, TYPE_CHECKING, Type
from typing import Any, Iterator, Optional, TYPE_CHECKING, Type
import yaml
from dcs.helicopters import helicopter_map
@@ -397,5 +396,5 @@ class AircraftType(UnitType[Type[FlyingType]]):
channel_namer=radio_config.channel_namer,
kneeboard_units=units,
utc_kneeboard=data.get("utc_kneeboard", False),
unit_class=UnitClass.Plane,
unit_class=UnitClass.PLANE,
)

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import logging
from dataclasses import dataclass
from pathlib import Path
from typing import Type, Optional, Iterator
from typing import Type, Iterator
import yaml
from dcs.unittype import VehicleType
@@ -55,8 +55,11 @@ class GroundUnitType(UnitType[Type[VehicleType]]):
introduction = "No data."
class_name = data.get("class")
# TODO Exception handling for missing classes
unit_class = UnitClass(class_name) if class_name else UnitClass.Unknown
if class_name is None:
logging.warning(f"{vehicle.id} has no class")
unit_class = UnitClass.UNKNOWN
else:
unit_class = UnitClass(class_name)
for variant in data.get("variants", [vehicle.id]):
yield GroundUnitType(

View File

@@ -1,15 +1,13 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Type, Optional, ClassVar, Iterator
from typing import Type, Iterator
import yaml
from dcs.ships import ship_map
from dcs.unittype import VehicleType, ShipType
from dcs.vehicles import vehicle_map
from dcs.unittype import ShipType
from game.data.units import UnitClass
from game.dcs.unittype import UnitType
@@ -70,5 +68,5 @@ class ShipUnitType(UnitType[Type[ShipType]]):
country_of_origin=data.get("origin", "No data."),
manufacturer=data.get("manufacturer", "No data."),
role=data.get("role", "No data."),
price=data.get("price", 1),
price=data.get("price"),
)

View File

@@ -1,157 +0,0 @@
from __future__ import annotations
import copy
import itertools
import random
from dataclasses import dataclass, field
from pathlib import Path
from typing import ClassVar, TYPE_CHECKING, Any, Iterator
import yaml
from game.data.groups import GroupRole, 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 gen.templates import GroundObjectTemplate
if TYPE_CHECKING:
from game import Game
from game.factions.faction import Faction
from game.theater import TheaterGroundObject, ControlPoint
@dataclass
class UnitGroup:
name: str
ground_units: list[GroundUnitType]
ship_units: list[ShipUnitType]
statics: list[str]
role: GroupRole
tasks: list[GroupTask] = field(default_factory=list)
template_names: list[str] = field(default_factory=list)
_by_name: ClassVar[dict[str, UnitGroup]] = {}
_by_role: ClassVar[dict[GroupRole, list[UnitGroup]]] = {}
_loaded: bool = False
_templates: list[GroundObjectTemplate] = field(default_factory=list)
def __str__(self) -> str:
return self.name
def update_from_unit_group(self, unit_group: UnitGroup) -> None:
# Update tasking and templates
self.tasks.extend([task for task in unit_group.tasks if task not in self.tasks])
self._templates.extend(
[
template
for template in unit_group.templates
if template not in self.templates
]
)
@property
def templates(self) -> list[GroundObjectTemplate]:
return self._templates
def add_template(self, faction_template: GroundObjectTemplate) -> None:
template = copy.deepcopy(faction_template)
updated_groups = []
for group in template.groups:
unit_types = list(
itertools.chain(
[u.dcs_id for u in self.ground_units if group.can_use_unit(u)],
[s.dcs_id for s in self.ship_units if group.can_use_unit(s)],
[s for s in self.statics if group.can_use_unit_type(s)],
)
)
if unit_types:
group.set_possible_types(unit_types)
updated_groups.append(group)
template.groups = updated_groups
self._templates.append(template)
def load_templates(self, faction: Faction) -> None:
self._templates = []
if self.template_names:
# Preferred templates
for template_name in self.template_names:
template = faction.templates.by_name(template_name)
if template:
self.add_template(template)
if not self._templates:
# Find all matching templates if no preferred set or available
for template in list(
faction.templates.for_role_and_tasks(self.role, self.tasks)
):
if any(self.has_unit_type(unit) for unit in template.units):
self.add_template(template)
def set_templates(self, templates: list[GroundObjectTemplate]) -> None:
self._templates = templates
def has_unit_type(self, unit_type: UnitType[Any]) -> bool:
return unit_type in self.ground_units or unit_type in self.ship_units
@property
def unit_types(self) -> Iterator[str]:
for unit in self.ground_units:
yield unit.dcs_id
for ship in self.ship_units:
yield ship.dcs_id
for static in self.statics:
yield static
@classmethod
def named(cls, name: str) -> UnitGroup:
if not cls._loaded:
cls._load_all()
return cls._by_name[name]
def generate(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
game: Game,
) -> TheaterGroundObject:
template = random.choice(self.templates)
return template.generate(name, position, control_point, game)
@classmethod
def _load_all(cls) -> None:
for file in Path("resources/units/unit_groups").glob("*.yaml"):
if not file.is_file():
continue
with file.open(encoding="utf-8") as data_file:
data = yaml.safe_load(data_file)
group_role = GroupRole(data.get("role"))
group_tasks = [GroupTask(n) for n in data.get("tasks", [])]
ground_units = [
GroundUnitType.named(n) for n in data.get("ground_units", [])
]
ship_units = [ShipUnitType.named(n) for n in data.get("ship_units", [])]
unit_group = UnitGroup(
name=data.get("name"),
ground_units=ground_units,
ship_units=ship_units,
statics=data.get("statics", []),
role=group_role,
tasks=group_tasks,
template_names=data.get("templates", []),
)
cls._by_name[unit_group.name] = unit_group
if group_role in cls._by_role:
cls._by_role[group_role].append(unit_group)
else:
cls._by_role[group_role] = [unit_group]
cls._loaded = True

View File

@@ -4,7 +4,7 @@ from abc import ABC
from collections import defaultdict
from dataclasses import dataclass
from functools import cached_property
from typing import TypeVar, Generic, Type, ClassVar, Any, Iterator, Optional
from typing import TypeVar, Generic, Type, ClassVar, Any, Iterator
from dcs.unittype import UnitType as DcsUnitType
@@ -26,7 +26,9 @@ class UnitType(ABC, Generic[DcsUnitTypeT]):
unit_class: UnitClass
_by_name: ClassVar[dict[str, UnitType[Any]]] = {}
_by_unit_type: ClassVar[dict[DcsUnitTypeT, list[UnitType[Any]]]] = defaultdict(list)
_by_unit_type: ClassVar[dict[Type[DcsUnitType], list[UnitType[Any]]]] = defaultdict(
list
)
_loaded: ClassVar[bool] = False
def __str__(self) -> str:
@@ -43,7 +45,7 @@ class UnitType(ABC, Generic[DcsUnitTypeT]):
@classmethod
def named(cls, name: str) -> UnitType[Any]:
raise NotImplementedError
return cls._by_name[name]
@classmethod
def for_dcs_type(cls, dcs_unit_type: DcsUnitTypeT) -> Iterator[UnitType[Any]]:

View File

@@ -26,7 +26,7 @@ if TYPE_CHECKING:
ConvoyUnit,
FlyingUnit,
FrontLineUnit,
GroundObjectMapping,
TheaterUnitMapping,
UnitMap,
SceneryObjectMapping,
)
@@ -72,8 +72,8 @@ class GroundLosses:
player_airlifts: List[AirliftUnits] = field(default_factory=list)
enemy_airlifts: List[AirliftUnits] = field(default_factory=list)
player_ground_objects: List[GroundObjectMapping] = field(default_factory=list)
enemy_ground_objects: List[GroundObjectMapping] = field(default_factory=list)
player_ground_objects: List[TheaterUnitMapping] = field(default_factory=list)
enemy_ground_objects: List[TheaterUnitMapping] = field(default_factory=list)
player_scenery: List[SceneryObjectMapping] = field(default_factory=list)
enemy_scenery: List[SceneryObjectMapping] = field(default_factory=list)
@@ -158,7 +158,7 @@ class Debriefing:
yield from self.ground_losses.enemy_airlifts
@property
def ground_object_losses(self) -> Iterator[GroundObjectMapping]:
def ground_object_losses(self) -> Iterator[TheaterUnitMapping]:
yield from self.ground_losses.player_ground_objects
yield from self.ground_losses.enemy_ground_objects
@@ -224,15 +224,7 @@ class Debriefing:
else:
losses = self.ground_losses.enemy_ground_objects
for loss in losses:
# We do not have handling for ships and statics UniType yet so we have to
# take more care here. Fallback for ship and static is to use the type str
# which is the dcs_type.id
unit_type = (
loss.ground_unit.unit_type.name
if loss.ground_unit.unit_type
else loss.ground_unit.type
)
losses_by_type[unit_type] += 1
losses_by_type[loss.theater_unit.type.id] += 1
return losses_by_type
def scenery_losses_by_type(self, player: bool) -> Dict[str, int]:
@@ -286,9 +278,9 @@ class Debriefing:
losses.enemy_cargo_ships.append(cargo_ship)
continue
ground_object = self.unit_map.ground_object(unit_name)
ground_object = self.unit_map.theater_units(unit_name)
if ground_object is not None:
if ground_object.ground_unit.ground_object.is_friendly(to_player=True):
if ground_object.theater_unit.ground_object.is_friendly(to_player=True):
losses.player_ground_objects.append(ground_object)
else:
losses.enemy_ground_objects.append(ground_object)

View File

@@ -1,21 +1,22 @@
from __future__ import annotations
import copy
import itertools
import logging
import random
from dataclasses import dataclass, field
from functools import cached_property
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING
import dcs
from dcs.countries import country_dict
from dcs.unittype import ShipType
from dcs.unittype import ShipType, StaticType
from dcs.unittype import UnitType as DcsUnitType
from game.data.building_data import (
WW2_ALLIES_BUILDINGS,
DEFAULT_AVAILABLE_BUILDINGS,
WW2_GERMANY_BUILDINGS,
WW2_FREE,
REQUIRED_BUILDINGS,
)
from game.data.doctrine import (
Doctrine,
@@ -24,18 +25,12 @@ from game.data.doctrine import (
WWII_DOCTRINE,
)
from game.data.units import UnitClass
from game.data.groups import GroupRole, GroupTask
from game import db
from game.data.groups import GroupRole
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.dcs.shipunittype import ShipUnitType
from game.dcs.unitgroup import UnitGroup
from game.armedforces.forcegroup import ForceGroup
from game.dcs.unittype import UnitType
from gen.templates import (
GroundObjectTemplates,
GroundObjectTemplate,
GroupTemplate,
)
if TYPE_CHECKING:
from game.theater.start_generator import ModSettings
@@ -84,7 +79,7 @@ class Faction:
air_defense_units: List[GroundUnitType] = field(default_factory=list)
# A list of all supported sets of units
preset_groups: list[UnitGroup] = field(default_factory=list)
preset_groups: list[ForceGroup] = field(default_factory=list)
# Possible Missile site generators for this faction
missiles: List[GroundUnitType] = field(default_factory=list)
@@ -110,7 +105,7 @@ class Faction:
# doctrine
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
# List of available building templates for this faction
# List of available building layouts for this faction
building_set: List[str] = field(default_factory=list)
# List of default livery overrides
@@ -125,47 +120,24 @@ class Faction:
#: both will use it.
unrestricted_satnav: bool = False
# All possible templates which can be generated by the faction
templates: GroundObjectTemplates = field(default=GroundObjectTemplates())
# All available unit_groups
unit_groups: dict[GroupRole, list[UnitGroup]] = field(default_factory=dict)
# Save all accessible units for performance increase
_accessible_units: list[UnitType[Any]] = field(default_factory=list)
def __getitem__(self, item: str) -> Any:
return getattr(self, item)
@property
def accessible_units(self) -> Iterator[UnitType[Any]]:
yield from self._accessible_units
@property
def air_defenses(self) -> list[str]:
"""Returns the Air Defense types"""
air_defenses = [a.name for a in self.air_defense_units]
air_defenses.extend(
[pg.name for pg in self.preset_groups if pg.role == GroupRole.AntiAir]
)
return sorted(air_defenses)
def has_access_to_unit_type(self, unit_type: str) -> bool:
# GroundUnits
if any(unit_type == u.dcs_id for u in self.accessible_units):
def has_access_to_dcs_type(self, unit_type: Type[DcsUnitType]) -> bool:
# Vehicle and Ship Units
if any(unit_type == u.dcs_unit_type for u in self.accessible_units):
return True
# Statics
if db.static_type_from_name(unit_type) is not None:
if issubclass(unit_type, StaticType):
# TODO Improve the statics checking
# We currently do not have any list or similar to check if a faction has
# access to a specific static. There we accept any static here
return True
return False
def has_access_to_unit_class(self, unit_class: UnitClass) -> bool:
return any(unit.unit_class is unit_class for unit in self.accessible_units)
def _load_accessible_units(self, templates: GroundObjectTemplates) -> None:
self._accessible_units = []
@cached_property
def accessible_units(self) -> list[UnitType[Any]]:
all_units: Iterator[UnitType[Any]] = itertools.chain(
self.ground_units,
self.infantry_units,
@@ -173,138 +145,22 @@ class Faction:
self.naval_units,
self.missiles,
(
ground_unit
unit
for preset_group in self.preset_groups
for ground_unit in preset_group.ground_units
),
(
ship_unit
for preset_group in self.preset_groups
for ship_unit in preset_group.ship_units
for unit in preset_group.units
),
)
for unit in all_units:
if unit not in self._accessible_units:
self._accessible_units.append(unit)
return list(all_units)
def initialize(
self, all_templates: GroundObjectTemplates, mod_settings: ModSettings
) -> None:
# Apply the mod settings
self._apply_mod_settings(mod_settings)
# Load all accessible units and store them for performant later usage
self._load_accessible_units(all_templates)
# Load all faction compatible templates
self._load_templates(all_templates)
# Load Unit Groups
self._load_unit_groups()
def _add_unit_group(self, unit_group: UnitGroup, merge: bool = True) -> None:
if not unit_group.templates:
unit_group.load_templates(self)
if not unit_group.templates:
# Empty templates will throw an error on generation
logging.error(
f"Skipping Unit group {unit_group.name} as no templates are available to generate the group"
)
return
if unit_group.role in self.unit_groups:
for group in self.unit_groups[unit_group.role]:
if merge and all(task in group.tasks for task in unit_group.tasks):
# Update existing group if same tasking
group.update_from_unit_group(unit_group)
return
# Add new Unit_group
self.unit_groups[unit_group.role].append(unit_group)
else:
self.unit_groups[unit_group.role] = [unit_group]
def _load_unit_groups(self) -> None:
# This function will create all the UnitGroups for the faction
# It will create a unit group for each global Template, Building or
# Legacy supported templates (not yet migrated from the generators).
# For every preset_group there will be a separate UnitGroup so no mixed
# UnitGroups will be generated for them. Special groups like complex SAM Systems
self.unit_groups = {}
# Generate UnitGroups for all global templates
for role, template in self.templates.templates:
if template.generic or role == GroupRole.Building:
# Build groups for global templates and buildings
self._add_group_for_template(role, template)
# Add preset groups
for preset_group in self.preset_groups:
# Add as separate group, do not merge with generic groups!
self._add_unit_group(preset_group, False)
def _add_group_for_template(
self, role: GroupRole, template: GroundObjectTemplate
) -> None:
unit_group = UnitGroup(
f"{role.value}: {', '.join([t.value for t in template.tasks])}",
[u for u in template.units if isinstance(u, GroundUnitType)],
[u for u in template.units if isinstance(u, ShipUnitType)],
list(template.statics),
role,
@property
def air_defenses(self) -> list[str]:
"""Returns the Air Defense types"""
# This is used for the faction overview in NewGameWizard
air_defenses = [a.name for a in self.air_defense_units]
air_defenses.extend(
[pg.name for pg in self.preset_groups if pg.role == GroupRole.AIR_DEFENSE]
)
unit_group.tasks = template.tasks
unit_group.set_templates([template])
self._add_unit_group(unit_group)
def initialize_group_template(
self, group: GroupTemplate, faction_sensitive: bool = True
) -> bool:
# Sensitive defines if the initialization should check if the unit is available
# to this faction or not. It is disabled for migration only atm.
unit_types = [
t
for t in group.unit_types
if not faction_sensitive or self.has_access_to_unit_type(t)
]
alternative_types = []
for accessible_unit in self.accessible_units:
if accessible_unit.unit_class in group.unit_classes:
unit_types.append(accessible_unit.dcs_id)
if accessible_unit.unit_class in group.alternative_classes:
alternative_types.append(accessible_unit.dcs_id)
if not unit_types and not alternative_types and not group.optional:
raise StopIteration
types = unit_types or alternative_types
group.set_possible_types(types)
return len(types) > 0
def _load_templates(self, all_templates: GroundObjectTemplates) -> None:
self.templates = GroundObjectTemplates()
# This loads all templates which are usable by the faction
for role, template in all_templates.templates:
# Make a deep copy of a template and add it to the template_list.
# This is required to have faction independent templates. Otherwise
# the reference would be the same and changes would affect all.
faction_template = copy.deepcopy(template)
try:
faction_template.groups[:] = [
group_template
for group_template in faction_template.groups
if self.initialize_group_template(group_template)
]
if (
role == GroupRole.Building
and GroupTask.StrikeTarget in template.tasks
and faction_template.category not in self.building_set
):
# Special handling for strike targets. Skip if not supported by faction
continue
if faction_template.groups:
self.templates.add_template(role, faction_template)
continue
except StopIteration:
pass
logging.info(f"{self.name} can not use template {template.name}")
return sorted(air_defenses)
@classmethod
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
@@ -350,8 +206,13 @@ class Faction:
]
faction.missiles = [GroundUnitType.named(n) for n in json.get("missiles", [])]
faction.naval_units = [
ShipUnitType.named(n) for n in json.get("naval_units", [])
]
# This has to be loaded AFTER GroundUnitType and ShipUnitType to work properly
faction.preset_groups = [
UnitGroup.named(n) for n in json.get("preset_groups", [])
ForceGroup.named(n) for n in json.get("preset_groups", [])
]
faction.requirements = json.get("requirements", {})
@@ -359,10 +220,6 @@ class Faction:
faction.carrier_names = json.get("carrier_names", [])
faction.helicopter_carrier_names = json.get("helicopter_carrier_names", [])
faction.naval_units = [
ShipUnitType.named(n) for n in json.get("naval_units", [])
]
faction.has_jtac = json.get("has_jtac", False)
jtac_name = json.get("jtac_unit", None)
if jtac_name is not None:
@@ -394,6 +251,9 @@ class Faction:
else:
faction.building_set = DEFAULT_AVAILABLE_BUILDINGS
# Add required buildings for the game logic (e.g. ammo, factory..)
faction.building_set.extend(REQUIRED_BUILDINGS)
# Load liveries override
faction.liveries_overrides = {}
liveries_overrides = json.get("liveries_overrides", {})
@@ -403,9 +263,6 @@ class Faction:
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
# Templates
faction.templates = GroundObjectTemplates()
return faction
@property
@@ -419,44 +276,7 @@ class Faction:
if unit.unit_class is unit_class:
yield unit
def groups_for_role_and_task(
self, group_role: GroupRole, group_task: Optional[GroupTask] = None
) -> list[UnitGroup]:
if group_role not in self.unit_groups:
return []
groups = []
for unit_group in self.unit_groups[group_role]:
if not group_task or group_task in unit_group.tasks:
groups.append(unit_group)
return groups
def groups_for_role_and_tasks(
self, group_role: GroupRole, tasks: list[GroupTask]
) -> list[UnitGroup]:
groups = []
for task in tasks:
for group in self.groups_for_role_and_task(group_role, task):
if group not in groups:
groups.append(group)
return groups
def random_group_for_role(self, group_role: GroupRole) -> Optional[UnitGroup]:
unit_groups = self.groups_for_role_and_task(group_role)
return random.choice(unit_groups) if unit_groups else None
def random_group_for_role_and_task(
self, group_role: GroupRole, group_task: GroupTask
) -> Optional[UnitGroup]:
unit_groups = self.groups_for_role_and_task(group_role, group_task)
return random.choice(unit_groups) if unit_groups else None
def random_group_for_role_and_tasks(
self, group_role: GroupRole, tasks: list[GroupTask]
) -> Optional[UnitGroup]:
unit_groups = self.groups_for_role_and_tasks(group_role, tasks)
return random.choice(unit_groups) if unit_groups else None
def _apply_mod_settings(self, mod_settings: ModSettings) -> None:
def apply_mod_settings(self, mod_settings: ModSettings) -> None:
# aircraft
if not mod_settings.a4_skyhawk:
self.remove_aircraft("A-4E-C")
@@ -516,20 +336,20 @@ class Faction:
self.remove_vehicle("KORNET")
# high digit sams
if not mod_settings.high_digit_sams:
self.remove_presets("SA-10B/S-300PS")
self.remove_presets("SA-12/S-300V")
self.remove_presets("SA-20/S-300PMU-1")
self.remove_presets("SA-20B/S-300PMU-2")
self.remove_presets("SA-23/S-300VM")
self.remove_presets("SA-17")
self.remove_presets("KS-19")
self.remove_preset("SA-10B/S-300PS")
self.remove_preset("SA-12/S-300V")
self.remove_preset("SA-20/S-300PMU-1")
self.remove_preset("SA-20B/S-300PMU-2")
self.remove_preset("SA-23/S-300VM")
self.remove_preset("SA-17")
self.remove_preset("KS-19")
def remove_aircraft(self, name: str) -> None:
for i in self.aircrafts:
if i.dcs_unit_type.id == name:
self.aircrafts.remove(i)
def remove_presets(self, name: str) -> None:
def remove_preset(self, name: str) -> None:
for pg in self.preset_groups:
if pg.name == name:
self.preset_groups.remove(pg)

View File

@@ -500,7 +500,7 @@ class Game:
return False
return True
def iads_considerate_culling(self, tgo: TheaterGroundObject[Any]) -> bool:
def iads_considerate_culling(self, tgo: TheaterGroundObject) -> bool:
if not self.settings.perf_do_not_cull_threatening_iads:
return self.position_culled(tgo.position)
else:

4
game/layout/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
from layout import TheaterLayout
from game.layout.layoutloader import LayoutLoader
LAYOUTS = LayoutLoader()

268
game/layout/layout.py Normal file
View File

@@ -0,0 +1,268 @@
from __future__ import annotations
import logging
import random
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Type
from dcs import Point
from dcs.unit import Unit
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.theatergroundobject import (
SamGroundObject,
EwrGroundObject,
BuildingGroundObject,
MissileSiteGroundObject,
ShipGroundObject,
CarrierGroundObject,
LhaGroundObject,
CoastalSiteGroundObject,
VehicleGroupGroundObject,
IadsGroundObject,
)
from game.theater.theatergroup import TheaterUnit
from game.utils import Heading
if TYPE_CHECKING:
from game.factions.faction import Faction
from game.theater.theatergroundobject import TheaterGroundObject
from game.theater.controlpoint import ControlPoint
class LayoutException(Exception):
pass
@dataclass
class LayoutUnit:
"""The Position and Orientation of a single unit within the GroupLayout"""
name: str
position: Point
heading: int
@staticmethod
def from_unit(unit: Unit) -> LayoutUnit:
"""Creates a LayoutUnit from a DCS Unit"""
return LayoutUnit(
unit.name,
Point(int(unit.position.x), int(unit.position.y)),
int(unit.heading),
)
@dataclass
class GroupLayout:
"""The Layout of a TheaterGroup"""
name: str
units: list[LayoutUnit]
# The group this template will be merged into
group: int = 1
# Define the amount of random units to be created by the randomizer.
# This can be a fixed int or a random value from a range of two ints as tuple
unit_count: list[int] = field(default_factory=list)
# defintion which unit types are supported
unit_types: list[Type[DcsUnitType]] = field(default_factory=list)
unit_classes: list[UnitClass] = field(default_factory=list)
alternative_classes: list[UnitClass] = field(default_factory=list)
# Defines if this groupTemplate is required or not
optional: bool = False
# if enabled the specific group will be generated during generation
# Can only be set to False if Optional = True
enabled: bool = True
# TODO Caching for faction!
def possible_types_for_faction(self, faction: Faction) -> list[Type[DcsUnitType]]:
"""TODO Description"""
unit_types = [t for t in self.unit_types if faction.has_access_to_dcs_type(t)]
alternative_types = []
for accessible_unit in faction.accessible_units:
if accessible_unit.unit_class in self.unit_classes:
unit_types.append(accessible_unit.dcs_unit_type)
if accessible_unit.unit_class in self.alternative_classes:
alternative_types.append(accessible_unit.dcs_unit_type)
if not unit_types and not alternative_types and not self.optional:
raise LayoutException
return unit_types or alternative_types
@property
def unit_counter(self) -> int:
"""TODO Documentation"""
default = len(self.units)
if self.unit_count:
if len(self.unit_count) == 1:
count = self.unit_count[0]
else:
count = random.choice(range(min(self.unit_count), max(self.unit_count)))
if count > default:
logging.error(
f"UnitCount for Group Layout {self.name} "
f"exceeds max available units for this group"
)
return default
return count
return default
@property
def max_size(self) -> int:
return len(self.units)
def generate_units(
self, go: TheaterGroundObject, unit_type: Type[DcsUnitType], amount: int
) -> list[TheaterUnit]:
"""TODO Documentation"""
return [
TheaterUnit.from_template(i, unit_type, self.units[i], go)
for i in range(amount)
]
class TheaterLayout:
"""TODO Documentation"""
def __init__(self, name: str, role: GroupRole, description: str = "") -> None:
self.name = name
self.role = role
self.description = description
self.tasks: list[GroupTask] = [] # The supported tasks
self.groups: list[GroupLayout] = []
# If the template is generic it will be used the generate the general
# UnitGroups during faction initialization. Generic Groups allow to be mixed
self.generic: bool = False
def usable_by_faction(self, faction: Faction) -> bool:
# Special handling for Buildings
if (
isinstance(self, BuildingLayout)
and self.category not in faction.building_set
):
return False
# Check if faction has at least 1 possible unit for non-optional groups
try:
return all(
len(group.possible_types_for_faction(faction)) > 0
for group in self.groups
if not group.optional
)
except LayoutException:
return False
def create_ground_object(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
) -> TheaterGroundObject:
"""TODO Documentation"""
raise NotImplementedError
def add_group(self, new_group: GroupLayout, index: int = 0) -> None:
"""Adds a group in the correct order to the template"""
if len(self.groups) > index:
self.groups.insert(index, new_group)
else:
self.groups.append(new_group)
@property
def size(self) -> int:
return sum([len(group.units) for group in self.groups])
class AntiAirLayout(TheaterLayout):
def create_ground_object(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
) -> IadsGroundObject:
if GroupTask.EARLY_WARNING_RADAR in self.tasks:
return EwrGroundObject(name, position, position.heading, control_point)
elif any(tasking in self.tasks for tasking in GroupRole.AIR_DEFENSE.tasks):
return SamGroundObject(name, position, position.heading, control_point)
raise RuntimeError(
f" No Template for AntiAir tasking ({', '.join(task.description for task in self.tasks)})"
)
class BuildingLayout(TheaterLayout):
def create_ground_object(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
) -> BuildingGroundObject:
return BuildingGroundObject(
name,
self.category,
position,
Heading.from_degrees(0),
control_point,
self.category == "fob",
)
@property
def category(self) -> str:
for task in self.tasks:
if task not in [GroupTask.STRIKE_TARGET, GroupTask.OFFSHORE_STRIKE_TARGET]:
return task.description.lower()
raise RuntimeError(f"Building Template {self.name} has no building category")
class NavalLayout(TheaterLayout):
def create_ground_object(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
) -> TheaterGroundObject:
if GroupTask.NAVY in self.tasks:
return ShipGroundObject(name, position, control_point)
elif GroupTask.AIRCRAFT_CARRIER in self.tasks:
return CarrierGroundObject(name, control_point)
elif GroupTask.HELICOPTER_CARRIER in self.tasks:
return LhaGroundObject(name, control_point)
raise NotImplementedError
class DefensesLayout(TheaterLayout):
def create_ground_object(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
) -> TheaterGroundObject:
if GroupTask.MISSILE in self.tasks:
return MissileSiteGroundObject(
name, position, position.heading, control_point
)
elif GroupTask.COASTAL in self.tasks:
return CoastalSiteGroundObject(
name, position, control_point, position.heading
)
raise NotImplementedError
class GroundForceLayout(TheaterLayout):
def create_ground_object(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
) -> TheaterGroundObject:
return VehicleGroupGroundObject(name, position, position.heading, control_point)

203
game/layout/layoutloader.py Normal file
View File

@@ -0,0 +1,203 @@
from __future__ import annotations
import itertools
import logging
import pickle
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from typing import Iterator
import dcs
import yaml
from dcs import Point
from dcs.unitgroup import StaticGroup
from game import persistency
from game.data.groups import GroupRole, GroupTask
from game.layout.layout import (
TheaterLayout,
GroupLayout,
LayoutUnit,
AntiAirLayout,
BuildingLayout,
NavalLayout,
GroundForceLayout,
DefensesLayout,
)
from game.layout.layoutmapping import GroupLayoutMapping, LayoutMapping
from game.profiling import logged_duration
from game.version import VERSION
TEMPLATE_DIR = "resources/layouts/"
TEMPLATE_DUMP = "Liberation/layouts.p"
TEMPLATE_TYPES = {
GroupRole.AIR_DEFENSE: AntiAirLayout,
GroupRole.BUILDING: BuildingLayout,
GroupRole.NAVAL: NavalLayout,
GroupRole.GROUND_FORCE: GroundForceLayout,
GroupRole.DEFENSES: DefensesLayout,
}
class LayoutLoader:
# list of layouts per category. e.g. AA or similar
_templates: dict[str, TheaterLayout] = {}
def __init__(self) -> None:
self._templates = {}
def initialize(self) -> None:
if not self._templates:
with logged_duration("Loading layouts"):
self.load_templates()
@property
def layouts(self) -> Iterator[TheaterLayout]:
self.initialize()
yield from self._templates.values()
def load_templates(self) -> None:
"""This will load all pre-loaded layouts from a pickle file.
If pickle can not be loaded it will import and dump the layouts"""
# We use a pickle for performance reasons. Importing takes many seconds
file = Path(persistency.base_path()) / TEMPLATE_DUMP
if file.is_file():
# Load from pickle if existing
with file.open("rb") as f:
try:
version, self._templates = pickle.load(f)
# Check if the game version of the dump is identical to the current
if version == VERSION:
return
except Exception as e:
logging.error(f"Error {e} reading layouts dump. Recreating.")
# If no dump is available or game version is different create a new dump
self.import_templates()
def import_templates(self) -> None:
"""This will import all layouts from the template folder
and dumps them to a pickle"""
mappings: dict[str, list[LayoutMapping]] = {}
with logged_duration("Parsing mapping yamls"):
for file in Path(TEMPLATE_DIR).rglob("*.yaml"):
if not file.is_file():
continue
with file.open("r", encoding="utf-8") as f:
mapping_dict = yaml.safe_load(f)
template_map = LayoutMapping.from_dict(mapping_dict, f.name)
if template_map.layout_file in mappings:
mappings[template_map.layout_file].append(template_map)
else:
mappings[template_map.layout_file] = [template_map]
with logged_duration(f"Parsing all layout miz multithreaded"):
with ThreadPoolExecutor() as exe:
for miz, maps in mappings.items():
exe.submit(self._load_from_miz, miz, maps)
logging.info(f"Imported {len(self._templates)} layouts")
self._dump_templates()
def _dump_templates(self) -> None:
file = Path(persistency.base_path()) / TEMPLATE_DUMP
dump = (VERSION, self._templates)
with file.open("wb") as fdata:
pickle.dump(dump, fdata)
@staticmethod
def mapping_for_group(
mappings: list[LayoutMapping], group_name: str
) -> tuple[LayoutMapping, int, GroupLayoutMapping]:
for mapping in mappings:
for g_id, group_mapping in enumerate(mapping.groups):
if (
group_mapping.name == group_name
or group_name in group_mapping.statics
):
return mapping, g_id, group_mapping
raise KeyError
def _load_from_miz(self, miz: str, mappings: list[LayoutMapping]) -> None:
template_position: dict[str, Point] = {}
temp_mis = dcs.Mission()
with logged_duration(f"Parsing {miz}"):
# The load_file takes a lot of time to compute. That's why the layouts
# are written to a pickle and can be reloaded from the ui
# Example the whole routine: 0:00:00.934417,
# the .load_file() method: 0:00:00.920409
temp_mis.load_file(miz)
for country in itertools.chain(
temp_mis.coalition["red"].countries.values(),
temp_mis.coalition["blue"].countries.values(),
):
for dcs_group in itertools.chain(
temp_mis.country(country.name).vehicle_group,
temp_mis.country(country.name).ship_group,
temp_mis.country(country.name).static_group,
):
try:
mapping, group_id, group_mapping = self.mapping_for_group(
mappings, dcs_group.name
)
except KeyError:
logging.warning(f"No mapping for dcs group {dcs_group.name}")
continue
template = self._templates.get(mapping.name, None)
if template is None:
# Create a new template
template = TEMPLATE_TYPES[mapping.role](
mapping.name, mapping.role, mapping.description
)
template.generic = mapping.generic
template.tasks = mapping.tasks
self._templates[template.name] = template
for i, unit in enumerate(dcs_group.units):
group_template = None
for group in template.groups:
if group.name == group_mapping.name:
# We already have a layoutgroup for this dcs_group
group_template = group
if not group_template:
group_template = GroupLayout(
group_mapping.name,
[],
group_mapping.group,
group_mapping.unit_count,
group_mapping.unit_types,
group_mapping.unit_classes,
group_mapping.alternative_classes,
)
group_template.optional = group_mapping.optional
# Add the group at the correct position
template.add_group(group_template, group_id)
unit_template = LayoutUnit.from_unit(unit)
if i == 0 and template.name not in template_position:
template_position[template.name] = unit.position
unit_template.position = (
unit_template.position - template_position[template.name]
)
group_template.units.append(unit_template)
def by_name(self, template_name: str) -> Iterator[TheaterLayout]:
for template in self.layouts:
if template.name == template_name:
yield template
def by_task(self, group_task: GroupTask) -> Iterator[TheaterLayout]:
for template in self.layouts:
if not group_task or group_task in template.tasks:
yield template
def by_tasks(self, group_tasks: list[GroupTask]) -> Iterator[TheaterLayout]:
unique_templates = []
for group_task in group_tasks:
for template in self.by_task(group_task):
if template not in unique_templates:
unique_templates.append(template)
yield from unique_templates

View File

@@ -0,0 +1,156 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Type
from dcs.unittype import UnitType as DcsUnitType
from game import db
from game.data.groups import GroupRole, GroupTask
from game.data.units import UnitClass
@dataclass
class GroupLayoutMapping:
# The group name used in the template.miz
name: str
# Defines if the group is required for the template or can be skipped
optional: bool = False
# All static units for the group
statics: list[str] = field(default_factory=list)
# Defines to which tgo group the groupTemplate will be added
# This allows to merge groups back together. Default: Merge all to group 1
group: int = field(default=1)
# How many units should be generated from the grouplayout. If only one value is
# added this will be an exact amount. If 2 values are used it will be a random
# amount between these values.
unit_count: list[int] = field(default_factory=list)
# All unit types the template supports.
unit_types: list[Type[DcsUnitType]] = field(default_factory=list)
# All unit classes the template supports.
unit_classes: list[UnitClass] = field(default_factory=list)
# TODO Clarify if this is required. Only used for EWRs to also Use SR when no
# dedicated EWRs are available to the faction
alternative_classes: list[UnitClass] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
d = self.__dict__
if not self.optional:
d.pop("optional")
if not self.statics:
d.pop("statics")
if not self.unit_types:
d.pop("unit_types")
if not self.unit_classes:
d.pop("unit_classes")
else:
d["unit_classes"] = [unit_class.value for unit_class in self.unit_classes]
if not self.alternative_classes:
d.pop("alternative_classes")
else:
d["alternative_classes"] = [
unit_class.value for unit_class in self.alternative_classes
]
if not self.unit_count:
d.pop("unit_count")
return d
@staticmethod
def from_dict(d: dict[str, Any]) -> GroupLayoutMapping:
optional = d["optional"] if "optional" in d else False
statics = d["statics"] if "statics" in d else []
unit_count = d["unit_count"] if "unit_count" in d else []
unit_types = []
if "unit_types" in d:
for u in d["unit_types"]:
unit_type = db.unit_type_from_name(u)
if unit_type:
unit_types.append(unit_type)
group = d["group"] if "group" in d else 1
unit_classes = (
[UnitClass(u) for u in d["unit_classes"]] if "unit_classes" in d else []
)
alternative_classes = (
[UnitClass(u) for u in d["alternative_classes"]]
if "alternative_classes" in d
else []
)
return GroupLayoutMapping(
d["name"],
optional,
statics,
group,
unit_count,
unit_types,
unit_classes,
alternative_classes,
)
@dataclass
class LayoutMapping:
# The name of the Template
name: str
# An optional description to give more information about the template
description: str
# Optional field to define if the template can be used to create generic groups
generic: bool
# The role the template can be used for
role: GroupRole
# All taskings the template can be used for
tasks: list[GroupTask]
# All Groups the template has
groups: list[GroupLayoutMapping]
# Define the miz file for the template. Optional. If empty use the mapping name
layout_file: str
def to_dict(self) -> dict[str, Any]:
d = {
"name": self.name,
"description": self.description,
"generic": self.generic,
"role": self.role.value,
"tasks": [task.description for task in self.tasks],
"groups": [group.to_dict() for group in self.groups],
"layout_file": self.layout_file,
}
if not self.description:
d.pop("description")
if not self.generic:
# Only save if true
d.pop("generic")
if not self.layout_file:
d.pop("layout_file")
return d
@staticmethod
def from_dict(d: dict[str, Any], file_name: str) -> LayoutMapping:
groups = [GroupLayoutMapping.from_dict(group) for group in d["groups"]]
description = d["description"] if "description" in d else ""
generic = d["generic"] if "generic" in d else False
layout_file = (
d["layout_file"] if "layout_file" in d else file_name.replace("yaml", "miz")
)
tasks = [GroupTask.by_description(task) for task in d["tasks"]]
return LayoutMapping(
d["name"],
description,
generic,
GroupRole(d["role"]),
tasks,
groups,
layout_file,
)

View File

@@ -12,7 +12,7 @@ from dcs.unitgroup import FlyingGroup
from game.ato import Flight, FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType
from game.missiongenerator.airsupport import AirSupport
from game.theater import MissionTarget, GroundUnit
from game.theater import MissionTarget, TheaterUnit
TARGET_WAYPOINTS = (
FlightWaypointType.TARGET_GROUP_LOC,
@@ -82,7 +82,7 @@ class PydcsWaypointBuilder:
return False
def register_special_waypoints(
self, targets: Iterable[Union[MissionTarget, GroundUnit]]
self, targets: Iterable[Union[MissionTarget, TheaterUnit]]
) -> None:
"""Create special target waypoints for various aircraft"""
for i, t in enumerate(targets):

View File

@@ -221,7 +221,7 @@ class FlotGenerator:
if self.game.settings.manpads:
# 50% of armored units protected by manpad
if random.choice([True, False]):
manpads = list(faction.infantry_with_class(UnitClass.Manpad))
manpads = list(faction.infantry_with_class(UnitClass.MANPAD))
if manpads:
u = random.choices(
manpads, weights=[m.spawn_weight for m in manpads]
@@ -237,10 +237,10 @@ class FlotGenerator:
)
return
possible_infantry_units = set(faction.infantry_with_class(UnitClass.Infantry))
possible_infantry_units = set(faction.infantry_with_class(UnitClass.INFANTRY))
if self.game.settings.manpads:
possible_infantry_units |= set(
faction.infantry_with_class(UnitClass.Manpad)
faction.infantry_with_class(UnitClass.MANPAD)
)
if not possible_infantry_units:
return

View File

@@ -40,7 +40,7 @@ from game.ato.flightwaypointtype import FlightWaypointType
from game.data.alic import AlicCodes
from game.dcs.aircrafttype import AircraftType
from game.radio.radios import RadioFrequency
from game.theater import ConflictTheater, LatLon, TheaterGroundObject, GroundUnit
from game.theater import ConflictTheater, LatLon, TheaterGroundObject, TheaterUnit
from game.theater.bullseye import Bullseye
from game.utils import Distance, UnitSystem, meters, mps, pounds
from game.weather import Weather
@@ -607,14 +607,14 @@ class SeadTaskPage(KneeboardPage):
self.theater = theater
@property
def target_units(self) -> Iterator[GroundUnit]:
def target_units(self) -> Iterator[TheaterUnit]:
if isinstance(self.flight.package.target, TheaterGroundObject):
yield from self.flight.package.target.strike_targets
@staticmethod
def alic_for(unit_type: str) -> str:
def alic_for(unit: TheaterUnit) -> str:
try:
return str(AlicCodes.code_for(unit_type))
return str(AlicCodes.code_for(unit))
except KeyError:
return ""
@@ -634,13 +634,13 @@ class SeadTaskPage(KneeboardPage):
writer.write(path)
def target_info_row(self, unit: GroundUnit) -> List[str]:
def target_info_row(self, unit: TheaterUnit) -> List[str]:
ll = self.theater.point_to_ll(unit.position)
unit_type = unit_type_from_name(unit.type)
unit_type = unit.type
name = unit.name if unit_type is None else unit_type.name
return [
name,
self.alic_for(unit.type),
self.alic_for(unit),
ll.format_dms(include_decimal_seconds=True),
]

View File

@@ -58,16 +58,14 @@ from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
from game.dcs.helpers import static_type_from_name, unit_type_from_name
from game.radio.radios import RadioFrequency, RadioRegistry
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
from game.theater import ControlPoint, TheaterGroundObject
from game.theater import ControlPoint, TheaterGroundObject, TheaterUnit
from game.theater.theatergroundobject import (
CarrierGroundObject,
GenericCarrierGroundObject,
LhaGroundObject,
MissileSiteGroundObject,
GroundGroup,
GroundUnit,
SceneryGroundUnit,
)
from game.theater.theatergroup import SceneryUnit, TheaterGroup
from game.unitmap import UnitMap
from game.utils import Heading, feet, knots, mps
from gen.runways import RunwayData
@@ -100,95 +98,114 @@ class GroundObjectGenerator:
def culled(self) -> bool:
return self.game.iads_considerate_culling(self.ground_object)
def generate(self, unique_name: bool = True) -> None:
def generate(self) -> None:
if self.culled:
return
for group in self.ground_object.groups:
if not group.units:
logging.warning(f"Found empty group in {self.ground_object}")
continue
group_name = group.group_name if unique_name else group.name
moving_group: Optional[MovingGroup[Any]] = None
for i, unit in enumerate(group.units):
if isinstance(unit, SceneryGroundUnit):
# Special handling for scenery objects:
# Only create a trigger zone and no "real" dcs unit
self.add_trigger_zone_for_scenery(unit)
continue
vehicle_units = []
ship_units = []
# 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)
elif unit.is_vehicle and unit.alive:
# All alive Vehicles
vehicle_units.append(unit)
elif unit.is_ship and unit.alive:
# All alive Ships
ship_units.append(unit)
if vehicle_units:
self.create_vehicle_group(group.group_name, vehicle_units)
if ship_units:
self.create_ship_group(group.group_name, ship_units)
# Only skip dead units after trigger zone for scenery created!
if not unit.alive:
continue
def create_vehicle_group(
self, group_name: str, units: list[TheaterUnit]
) -> VehicleGroup:
vehicle_group: Optional[VehicleGroup] = None
for unit in units:
assert issubclass(unit.type, VehicleType)
if vehicle_group is None:
vehicle_group = self.m.vehicle_group(
self.country,
group_name,
unit.type,
position=unit.position,
heading=unit.position.heading.degrees,
)
vehicle_group.units[0].player_can_drive = True
self.enable_eplrs(vehicle_group, unit.type)
vehicle_group.units[0].name = unit.unit_name
self.set_alarm_state(vehicle_group)
else:
vehicle_unit = Vehicle(
self.m.next_unit_id(),
unit.unit_name,
unit.type.id,
)
vehicle_unit.player_can_drive = True
vehicle_unit.position = unit.position
vehicle_unit.heading = unit.position.heading.degrees
vehicle_group.add_unit(vehicle_unit)
self._register_theater_unit(unit, vehicle_group.units[-1])
if vehicle_group is None:
raise RuntimeError(f"Error creating VehicleGroup for {group_name}")
return vehicle_group
unit_type = unit_type_from_name(unit.type)
if not unit_type:
raise RuntimeError(
f"Unit type {unit.type} is not a valid dcs unit type"
)
def create_ship_group(
self,
group_name: str,
units: list[TheaterUnit],
frequency: Optional[RadioFrequency] = None,
) -> ShipGroup:
ship_group: Optional[ShipGroup] = None
for unit in units:
assert issubclass(unit.type, ShipType)
if ship_group is None:
ship_group = self.m.ship_group(
self.country,
group_name,
unit.type,
position=unit.position,
heading=unit.position.heading.degrees,
)
if frequency:
ship_group.set_frequency(frequency.hertz)
ship_group.units[0].name = unit.unit_name
self.set_alarm_state(ship_group)
else:
ship_unit = Ship(
self.m.next_unit_id(),
unit.unit_name,
unit.type,
)
if frequency:
ship_unit.set_frequency(frequency.hertz)
ship_unit.position = unit.position
ship_unit.heading = unit.position.heading.degrees
ship_group.add_unit(ship_unit)
self._register_theater_unit(unit, ship_group.units[-1])
if ship_group is None:
raise RuntimeError(f"Error creating ShipGroup for {group_name}")
return ship_group
unit_name = unit.unit_name if unique_name else unit.name
if moving_group is None or group.static_group:
# First unit of the group will create the dcs group
if issubclass(unit_type, VehicleType):
moving_group = self.m.vehicle_group(
self.country,
group_name,
unit_type,
position=unit.position,
heading=unit.position.heading.degrees,
)
moving_group.units[0].player_can_drive = True
self.enable_eplrs(moving_group, unit_type)
elif issubclass(unit_type, ShipType):
moving_group = self.m.ship_group(
self.country,
group_name,
unit_type,
position=unit.position,
heading=unit.position.heading.degrees,
)
elif issubclass(unit_type, StaticType):
static_group = self.m.static_group(
country=self.country,
name=unit_name,
_type=unit_type,
position=unit.position,
heading=unit.position.heading.degrees,
dead=not unit.alive,
)
self._register_ground_unit(unit, static_group.units[0])
continue
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
if moving_group:
moving_group.units[0].name = unit_name
self.set_alarm_state(moving_group)
self._register_ground_unit(unit, moving_group.units[0])
else:
raise RuntimeError("DCS Group creation failed")
else:
# Additional Units in the group
dcs_unit: Optional[Unit] = None
if issubclass(unit_type, VehicleType):
dcs_unit = Vehicle(
self.m.next_unit_id(),
unit_name,
unit.type,
)
dcs_unit.player_can_drive = True
elif issubclass(unit_type, ShipType):
dcs_unit = Ship(
self.m.next_unit_id(),
unit_name,
unit_type,
)
if dcs_unit:
dcs_unit.position = unit.position
dcs_unit.heading = unit.position.heading.degrees
moving_group.add_unit(dcs_unit)
self._register_ground_unit(unit, dcs_unit)
else:
raise RuntimeError("DCS Unit creation failed")
static_group = self.m.static_group(
country=self.country,
name=unit.unit_name,
_type=unit.type,
position=unit.position,
heading=unit.position.heading.degrees,
dead=not unit.alive,
)
self._register_theater_unit(unit, static_group.units[0])
@staticmethod
def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
@@ -201,14 +218,14 @@ class GroundObjectGenerator:
else:
group.points[0].tasks.append(OptAlarmState(1))
def _register_ground_unit(
def _register_theater_unit(
self,
ground_unit: GroundUnit,
theater_unit: TheaterUnit,
dcs_unit: Unit,
) -> None:
self.unit_map.add_ground_object_mapping(ground_unit, dcs_unit)
self.unit_map.add_theater_unit_mapping(theater_unit, dcs_unit)
def add_trigger_zone_for_scenery(self, scenery: SceneryGroundUnit) -> None:
def add_trigger_zone_for_scenery(self, scenery: SceneryUnit) -> None:
# Align the trigger zones to the faction color on the DCS briefing/F10 map.
color = (
{1: 0.2, 2: 0.7, 3: 1, 4: 0.15}
@@ -265,7 +282,7 @@ class MissileSiteGenerator(GroundObjectGenerator):
# culled despite being a threat.
return False
def generate(self, unique_name: bool = True) -> None:
def generate(self) -> None:
super(MissileSiteGenerator, self).generate()
# Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now)
# TODO : Should be pre-planned ?
@@ -347,7 +364,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
self.icls_alloc = icls_alloc
self.runways = runways
def generate(self, unique_name: bool = True) -> None:
def generate(self) -> None:
# This can also be refactored as the general generation was updated
atc = self.radio_registry.alloc_uhf()
@@ -357,40 +374,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
logging.warning(f"Found empty carrier group in {self.control_point}")
continue
# Correct unit type for the carrier.
# This is only used for the super carrier setting
unit_type = (
self.get_carrier_type(group)
if g_id == 0
else ship_map[group.units[0].type]
)
ship_group = self.m.ship_group(
self.country,
group.group_name if unique_name else group.name,
unit_type,
position=group.units[0].position,
heading=group.units[0].position.heading.degrees,
)
ship_group.set_frequency(atc.hertz)
ship_group.units[0].name = (
group.units[0].unit_name if unique_name else group.units[0].name
)
self._register_ground_unit(group.units[0], ship_group.units[0])
for unit in group.units[1:]:
ship = Ship(
self.m.next_unit_id(),
unit.unit_name if unique_name else unit.name,
unit_type_from_name(unit.type),
)
ship.position.x = unit.position.x
ship.position.y = unit.position.y
ship.heading = unit.position.heading.degrees
ship.set_frequency(atc.hertz)
ship_group.add_unit(ship)
self._register_ground_unit(unit, ship)
ship_group = self.create_ship_group(group.group_name, group.units, atc)
# Always steam into the wind, even if the carrier is being moved.
# There are multiple unsimulated hours between turns, so we can
@@ -400,19 +384,24 @@ class GenericCarrierGenerator(GroundObjectGenerator):
# Set Carrier Specific Options
if g_id == 0:
# Correct unit type for the carrier.
# This is only used for the super carrier setting
ship_group.units[0].type = self.get_carrier_type(group).id
tacan = self.tacan_registry.alloc_for_band(
TacanBand.X, TacanUsage.TransmitReceive
)
tacan_callsign = self.tacan_callsign()
icls = next(self.icls_alloc)
self.activate_beacons(ship_group, tacan, tacan_callsign, icls)
self.add_runway_data(
brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls
)
def get_carrier_type(self, group: GroundGroup) -> Type[ShipType]:
return ship_map[group.units[0].type]
def get_carrier_type(self, group: TheaterGroup) -> Type[ShipType]:
carrier_type = group.units[0].type
if issubclass(carrier_type, ShipType):
return carrier_type
raise RuntimeError(f"First unit of TGO {group.name} is no Ship")
def steam_into_wind(self, group: ShipGroup) -> Optional[Heading]:
wind = self.game.conditions.weather.wind.at_0m
@@ -479,7 +468,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
class CarrierGenerator(GenericCarrierGenerator):
"""Generator for CV(N) groups."""
def get_carrier_type(self, group: GroundGroup) -> Type[ShipType]:
def get_carrier_type(self, group: TheaterGroup) -> Type[ShipType]:
unit_type = super().get_carrier_type(group)
if self.game.settings.supercarrier:
unit_type = self.upgrade_to_supercarrier(unit_type, self.control_point.name)

View File

@@ -176,7 +176,7 @@ class ProcurementAi:
worst_fulfillment = fulfillment
worst_balanced = unit_class
if worst_balanced is None:
return UnitClass.Tank
return UnitClass.TANK
return worst_balanced
@staticmethod

View File

@@ -132,7 +132,7 @@ class MissionResultsProcessor:
@staticmethod
def commit_ground_losses(debriefing: Debriefing) -> None:
for ground_object_loss in debriefing.ground_object_losses:
ground_object_loss.ground_unit.kill()
ground_object_loss.theater_unit.kill()
for scenery_object_loss in debriefing.scenery_object_losses:
scenery_object_loss.ground_unit.kill()

View File

@@ -20,13 +20,11 @@ from typing import (
Set,
TYPE_CHECKING,
Tuple,
Union,
)
from dcs.mapping import Point
from dcs.ships import Forrestal, KUZNECOW, LHA_Tarawa, Stennis, Type_071
from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unit import Unit
from dcs.unitgroup import ShipGroup, StaticGroup
from game.dcs.helpers import unit_type_from_name
@@ -39,14 +37,10 @@ from gen.runways import RunwayAssigner, RunwayData
from .base import Base
from .missiontarget import MissionTarget
from .theatergroundobject import (
BuildingGroundObject,
GenericCarrierGroundObject,
TheaterGroundObject,
BuildingGroundObject,
CarrierGroundObject,
LhaGroundObject,
GroundUnit,
)
from .theatergroup import TheaterUnit
from ..ato.starttype import StartType
from ..data.units import UnitClass
from ..dcs.aircrafttype import AircraftType
@@ -525,8 +519,8 @@ class ControlPoint(MissionTarget, ABC):
for group in g.groups:
for u in group.units:
if u.unit_type and u.unit_type.unit_class in [
UnitClass.AircraftCarrier,
UnitClass.HelicopterCarrier,
UnitClass.AIRCRAFT_CARRIER,
UnitClass.HELICOPTER_CARRIER,
]:
return group.group_name
return None
@@ -816,28 +810,26 @@ class ControlPoint(MissionTarget, ABC):
return self.front_line_capacity_with(self.active_ammo_depots_count)
@property
def all_ammo_depots(self) -> Iterator[BuildingGroundObject]:
def all_ammo_depots(self) -> Iterator[TheaterGroundObject]:
for tgo in self.connected_objectives:
if not tgo.is_ammo_depot:
continue
assert isinstance(tgo, BuildingGroundObject)
yield tgo
@property
def active_ammo_depots(self) -> Iterator[BuildingGroundObject]:
for tgo in self.all_ammo_depots:
if not tgo.is_dead:
if tgo.is_ammo_depot:
yield tgo
def ammo_depot_count(self, alive_only: bool = False) -> int:
return sum(
ammo_depot.alive_unit_count if alive_only else ammo_depot.unit_count
for ammo_depot in self.all_ammo_depots
)
@property
def active_ammo_depots_count(self) -> int:
"""Return the number of available ammo depots"""
return len(list(self.active_ammo_depots))
return self.ammo_depot_count(True)
@property
def total_ammo_depots_count(self) -> int:
"""Return the number of ammo depots, including dead ones"""
return len(list(self.all_ammo_depots))
return self.ammo_depot_count()
@property
def active_fuel_depots_count(self) -> int:
@@ -856,7 +848,7 @@ class ControlPoint(MissionTarget, ABC):
return len([obj for obj in self.connected_objectives if obj.category == "fuel"])
@property
def strike_targets(self) -> list[GroundUnit]:
def strike_targets(self) -> list[TheaterUnit]:
return []
@property
@@ -1008,7 +1000,7 @@ class NavalControlPoint(ControlPoint, ABC):
# while its escorts are still alive.
for group in self.find_main_tgo().groups:
for u in group.units:
if unit_type_from_name(u.type) in [
if u.type in [
Forrestal,
Stennis,
LHA_Tarawa,

View File

@@ -8,7 +8,7 @@ from dcs.unit import Unit
if TYPE_CHECKING:
from game.ato.flighttype import FlightType
from game.theater.theatergroundobject import GroundUnit
from game.theater import TheaterUnit
class MissionTarget:
@@ -47,5 +47,5 @@ class MissionTarget:
]
@property
def strike_targets(self) -> list[GroundUnit]:
def strike_targets(self) -> list[TheaterUnit]:
return []

View File

@@ -6,19 +6,18 @@ from dataclasses import dataclass
from datetime import datetime
from typing import List, Optional
import dcs.statics
from game import Game
from game.factions.faction import Faction
from game.scenery_group import SceneryGroup
from game.theater import PointWithHeading
from game.theater.theatergroundobject import (
AirDefenseRange,
BuildingGroundObject,
SceneryGroundUnit,
GroundGroup,
)
from .theatergroup import SceneryUnit, TheaterGroup
from game.utils import Heading
from game.version import VERSION
from gen.templates import GroundObjectTemplates, GroundObjectTemplate
from gen.naming import namegen
from . import (
ConflictTheater,
@@ -28,9 +27,9 @@ from . import (
OffMapSpawn,
)
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
from ..data.units import UnitClass
from ..data.groups import GroupRole, GroupTask, ROLE_TASKINGS
from ..dcs.unitgroup import UnitGroup
from ..data.groups import GroupRole, GroupTask
from ..armedforces.forcegroup import ForceGroup
from ..armedforces.armedforces import ArmedForces
from ..profiling import logged_duration
from ..settings import Settings
@@ -77,9 +76,8 @@ class GameGenerator:
self.air_wing_config = air_wing_config
self.settings = settings
self.generator_settings = generator_settings
with logged_duration(f"Initializing faction and templates"):
self.initialize_factions(mod_settings)
self.player.apply_mod_settings(mod_settings)
self.enemy.apply_mod_settings(mod_settings)
def generate(self) -> Game:
with logged_duration("TGO population"):
@@ -126,12 +124,6 @@ class GameGenerator:
for cp in to_remove:
self.theater.controlpoints.remove(cp)
def initialize_factions(self, mod_settings: ModSettings) -> None:
with logged_duration("Loading Templates from mapping"):
templates = GroundObjectTemplates.from_folder("resources/templates/")
self.player.initialize(templates, mod_settings)
self.enemy.initialize(templates, mod_settings)
class ControlPointGroundObjectGenerator:
def __init__(
@@ -152,35 +144,30 @@ class ControlPointGroundObjectGenerator:
def faction(self) -> Faction:
return self.game.coalition_for(self.control_point.captured).faction
@property
def armed_forces(self) -> ArmedForces:
return self.game.coalition_for(self.control_point.captured).armed_forces
def generate(self) -> bool:
self.control_point.connected_objectives = []
self.generate_navy()
return True
def generate_random_ground_object(
self, unit_groups: list[UnitGroup], position: PointWithHeading
self, unit_groups: list[ForceGroup], position: PointWithHeading
) -> None:
self.generate_ground_object_from_group(random.choice(unit_groups), position)
def generate_ground_object_from_group(
self, unit_group: UnitGroup, position: PointWithHeading
self, unit_group: ForceGroup, position: PointWithHeading
) -> None:
try:
with logged_duration(
f"Ground Object generation for unit_group "
f"{unit_group.name} ({unit_group.role.value})"
):
ground_object = unit_group.generate(
namegen.random_objective_name(),
position,
self.control_point,
self.game,
)
self.control_point.connected_objectives.append(ground_object)
except NotImplementedError:
logging.error("Template Generator not implemented yet")
except IndexError:
logging.error(f"No templates to generate object from {unit_group.name}")
ground_object = unit_group.generate(
namegen.random_objective_name(),
position,
self.control_point,
self.game,
)
self.control_point.connected_objectives.append(ground_object)
def generate_navy(self) -> None:
skip_player_navy = self.generator_settings.no_player_navy
@@ -190,11 +177,9 @@ class ControlPointGroundObjectGenerator:
if not self.control_point.captured and skip_enemy_navy:
return
for position in self.control_point.preset_locations.ships:
unit_group = self.faction.random_group_for_role_and_task(
GroupRole.Naval, GroupTask.Navy
)
unit_group = self.armed_forces.random_group_for_task(GroupTask.NAVY)
if not unit_group:
logging.error(f"{self.faction_name} has no UnitGroup for Navy")
logging.warning(f"{self.faction_name} has no ForceGroup for Navy")
return
self.generate_ground_object_from_group(unit_group, position)
@@ -217,11 +202,9 @@ class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
)
return False
unit_group = self.faction.random_group_for_role_and_task(
GroupRole.Naval, GroupTask.AircraftCarrier
)
unit_group = self.armed_forces.random_group_for_task(GroupTask.AIRCRAFT_CARRIER)
if not unit_group:
logging.error(f"{self.faction_name} has no UnitGroup for AircraftCarrier")
logging.error(f"{self.faction_name} has no access to AircraftCarrier")
return False
self.generate_ground_object_from_group(
unit_group,
@@ -246,11 +229,11 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
)
return False
unit_group = self.faction.random_group_for_role_and_task(
GroupRole.Naval, GroupTask.HelicopterCarrier
unit_group = self.armed_forces.random_group_for_task(
GroupTask.HELICOPTER_CARRIER
)
if not unit_group:
logging.error(f"{self.faction_name} has no UnitGroup for HelicopterCarrier")
logging.error(f"{self.faction_name} has no access to HelicopterCarrier")
return False
self.generate_ground_object_from_group(
unit_group,
@@ -293,11 +276,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
def generate_armor_groups(self) -> None:
for position in self.control_point.preset_locations.armor_groups:
unit_group = self.faction.random_group_for_role_and_tasks(
GroupRole.GroundForce, ROLE_TASKINGS[GroupRole.GroundForce]
)
unit_group = self.armed_forces.random_group_for_task(GroupTask.BASE_DEFENSE)
if not unit_group:
logging.error(f"{self.faction_name} has no templates for Armor Groups")
logging.error(f"{self.faction_name} has no ForceGroup for Armor")
return
self.generate_ground_object_from_group(unit_group, position)
@@ -318,11 +299,11 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
def generate_ewrs(self) -> None:
for position in self.control_point.preset_locations.ewrs:
unit_group = self.faction.random_group_for_role_and_task(
GroupRole.AntiAir, GroupTask.EWR
unit_group = self.armed_forces.random_group_for_task(
GroupTask.EARLY_WARNING_RADAR
)
if not unit_group:
logging.error(f"{self.faction_name} has no UnitGroup for EWR")
logging.error(f"{self.faction_name} has no ForceGroup for EWR")
return
self.generate_ground_object_from_group(unit_group, position)
@@ -331,31 +312,27 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
group_task: GroupTask,
position: PointWithHeading,
) -> None:
unit_group = self.faction.random_group_for_role_and_task(
GroupRole.Building, group_task
)
# GroupTask is the type of the building to be generated
unit_group = self.armed_forces.random_group_for_task(group_task)
if not unit_group:
logging.error(
f"{self.faction_name} has no access to Building ({group_task.value})"
raise RuntimeError(
f"{self.faction_name} has no access to Building {group_task.description}"
)
return
self.generate_ground_object_from_group(unit_group, position)
def generate_ammunition_depots(self) -> None:
for position in self.control_point.preset_locations.ammunition_depots:
self.generate_building_at(GroupTask.Ammo, position)
self.generate_building_at(GroupTask.AMMO, position)
def generate_factories(self) -> None:
for position in self.control_point.preset_locations.factories:
self.generate_building_at(GroupTask.Factory, position)
self.generate_building_at(GroupTask.FACTORY, position)
def generate_aa_at(
self, position: PointWithHeading, tasks: list[GroupTask]
) -> None:
for task in tasks:
unit_group = self.faction.random_group_for_role_and_task(
GroupRole.AntiAir, task
)
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
@@ -363,7 +340,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
return
logging.error(
f"{self.faction_name} has no access to SAM Templates ({', '.join([task.value for task in tasks])})"
f"{self.faction_name} has no access to SAM {', '.join([task.description for task in tasks])}"
)
def generate_scenery_sites(self) -> None:
@@ -380,21 +357,20 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
Heading.from_degrees(0),
self.control_point,
)
ground_group = GroundGroup(
ground_group = TheaterGroup(
self.game.next_group_id(),
scenery.zone_def.name,
PointWithHeading.from_point(scenery.position, Heading.from_degrees(0)),
[],
g,
)
ground_group.static_group = True
g.groups.append(ground_group)
# Each nested trigger zone is a target/building/unit for an objective.
for zone in scenery.zones:
scenery_unit = SceneryGroundUnit(
scenery_unit = SceneryUnit(
zone.id,
zone.name,
"",
dcs.statics.Fortification.White_Flag,
PointWithHeading.from_point(zone.position, Heading.from_degrees(0)),
g,
)
@@ -405,31 +381,27 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
def generate_missile_sites(self) -> None:
for position in self.control_point.preset_locations.missile_sites:
unit_group = self.faction.random_group_for_role_and_task(
GroupRole.Defenses, GroupTask.Missile
)
unit_group = self.armed_forces.random_group_for_task(GroupTask.MISSILE)
if not unit_group:
logging.error(f"{self.faction_name} has no UnitGroup for Missile")
logging.warning(f"{self.faction_name} has no ForceGroup for Missile")
return
self.generate_ground_object_from_group(unit_group, position)
def generate_coastal_sites(self) -> None:
for position in self.control_point.preset_locations.coastal_defenses:
unit_group = self.faction.random_group_for_role_and_task(
GroupRole.Defenses, GroupTask.Coastal
)
unit_group = self.armed_forces.random_group_for_task(GroupTask.COASTAL)
if not unit_group:
logging.error(f"{self.faction_name} has no UnitGroup for Coastal")
logging.warning(f"{self.faction_name} has no ForceGroup for Coastal")
return
self.generate_ground_object_from_group(unit_group, position)
def generate_strike_targets(self) -> None:
for position in self.control_point.preset_locations.strike_locations:
self.generate_building_at(GroupTask.StrikeTarget, position)
self.generate_building_at(GroupTask.STRIKE_TARGET, position)
def generate_offshore_strike_targets(self) -> None:
for position in self.control_point.preset_locations.offshore_strike_locations:
self.generate_building_at(GroupTask.Oil, position)
self.generate_building_at(GroupTask.OIL, position)
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):

View File

@@ -3,28 +3,20 @@ from __future__ import annotations
import itertools
import logging
from abc import ABC
from collections.abc import Sequence
from dataclasses import dataclass
from enum import Enum
from typing import Iterator, List, TYPE_CHECKING, Union, Optional, Any
from typing import Iterator, List, TYPE_CHECKING
from dcs.unittype import VehicleType, ShipType
from dcs.unittype import VehicleType
from dcs.vehicles import vehicle_map
from dcs.ships import ship_map
from dcs.mapping import Point
from dcs.triggers import TriggerZone
from game.dcs.helpers import unit_type_from_name
from ..data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS
from ..dcs.groundunittype import GroundUnitType
from ..dcs.shipunittype import ShipUnitType
from ..dcs.unittype import UnitType
from ..point_with_heading import PointWithHeading
from ..utils import Distance, Heading, meters
if TYPE_CHECKING:
from gen.templates import UnitTemplate, GroupTemplate
from .theatergroup import TheaterUnit, TheaterGroup
from .controlpoint import ControlPoint
from ..ato.flighttype import FlightType
@@ -53,126 +45,6 @@ NAME_BY_CATEGORY = {
}
class SkynetRole(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.
SamAsEwr = "SamAsEwr"
#: An air defense unit that should be used as point defense by Skynet.
PointDefense = "PD"
#: 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.
NoSkynetBehavior = "NoSkynetBehavior"
class AirDefenseRange(Enum):
AAA = ("AAA", SkynetRole.NoSkynetBehavior)
Short = ("short", SkynetRole.NoSkynetBehavior)
Medium = ("medium", SkynetRole.Sam)
Long = ("long", SkynetRole.SamAsEwr)
def __init__(self, description: str, default_role: SkynetRole) -> None:
self.range_name = description
self.default_role = default_role
@dataclass
class GroundUnit:
# Units can be everything.. Static, Vehicle, Ship.
id: int
name: str
type: str # dcs.UnitType as string
position: PointWithHeading
ground_object: TheaterGroundObject
alive: bool = True
_unit_type: Optional[UnitType[Any]] = None
@staticmethod
def from_template(
id: int, unit_type: str, t: UnitTemplate, go: TheaterGroundObject
) -> GroundUnit:
return GroundUnit(
id,
t.name,
unit_type,
PointWithHeading.from_point(t.position, Heading.from_degrees(t.heading)),
go,
)
@property
def unit_type(self) -> Optional[UnitType[Any]]:
if not self._unit_type:
try:
unit_type: Optional[UnitType[Any]] = None
dcs_type = db.unit_type_from_name(self.type)
if dcs_type and issubclass(dcs_type, VehicleType):
unit_type = next(GroundUnitType.for_dcs_type(dcs_type))
elif dcs_type and issubclass(dcs_type, ShipType):
unit_type = next(ShipUnitType.for_dcs_type(dcs_type))
self._unit_type = unit_type
except StopIteration:
logging.error(f"No UnitType for {self.type}")
pass
return self._unit_type
def kill(self) -> None:
self.alive = False
@property
def unit_name(self) -> str:
return f"{str(self.id).zfill(4)} | {self.name}"
@property
def display_name(self) -> str:
dead_label = " [DEAD]" if not self.alive else ""
unit_label = self.unit_type or self.type or self.name
return f"{str(self.id).zfill(4)} | {unit_label}{dead_label}"
class SceneryGroundUnit(GroundUnit):
"""Special GroundUnit for handling scenery ground objects"""
zone: TriggerZone
@dataclass
class GroundGroup:
id: int
name: str
position: PointWithHeading
units: list[GroundUnit]
ground_object: TheaterGroundObject
static_group: bool = False
@staticmethod
def from_template(
id: int,
g: GroupTemplate,
go: TheaterGroundObject,
) -> GroundGroup:
tgo_group = GroundGroup(
id,
g.name,
PointWithHeading.from_point(go.position, go.heading),
g.generate_units(go),
go,
)
tgo_group.static_group = g.static
return tgo_group
@property
def group_name(self) -> str:
return f"{str(self.id).zfill(4)} | {self.name}"
@property
def alive_units(self) -> int:
return sum([unit.alive for unit in self.units])
class TheaterGroundObject(MissionTarget):
def __init__(
self,
@@ -188,27 +60,28 @@ class TheaterGroundObject(MissionTarget):
self.heading = heading
self.control_point = control_point
self.sea_object = sea_object
self.groups: List[GroundGroup] = []
self.groups: List[TheaterGroup] = []
@property
def is_dead(self) -> bool:
return self.alive_unit_count == 0
@property
def units(self) -> Iterator[GroundUnit]:
def units(self) -> Iterator[TheaterUnit]:
"""
:return: all the units at this location
"""
yield from itertools.chain.from_iterable([g.units for g in self.groups])
@property
def statics(self) -> Iterator[GroundUnit]:
def statics(self) -> Iterator[TheaterUnit]:
for group in self.groups:
if group.static_group:
yield from group.units
for unit in group.units:
if unit.is_static:
yield unit
@property
def dead_units(self) -> list[GroundUnit]:
def dead_units(self) -> list[TheaterUnit]:
"""
:return: all the dead units at this location
"""
@@ -253,6 +126,10 @@ class TheaterGroundObject(MissionTarget):
]
yield from super().mission_types(for_player)
@property
def unit_count(self) -> int:
return sum([g.unit_count for g in self.groups])
@property
def alive_unit_count(self) -> int:
return sum([g.alive_units for g in self.groups])
@@ -269,20 +146,15 @@ class TheaterGroundObject(MissionTarget):
return True
return False
def _max_range_of_type(self, group: GroundGroup, range_type: str) -> Distance:
def _max_range_of_type(self, group: TheaterGroup, range_type: str) -> Distance:
if not self.might_have_aa:
return meters(0)
max_range = meters(0)
for u in group.units:
unit = unit_type_from_name(u.type)
if unit is None:
logging.error(f"Unknown unit type {u.type}")
continue
# Some units in pydcs have detection_range/threat_range defined,
# but explicitly set to None.
unit_range = getattr(unit, range_type, None)
unit_range = getattr(u.type, range_type, None)
if unit_range is not None:
max_range = max(max_range, meters(unit_range))
return max_range
@@ -290,7 +162,7 @@ class TheaterGroundObject(MissionTarget):
def max_detection_range(self) -> Distance:
return max(self.detection_range(g) for g in self.groups)
def detection_range(self, group: GroundGroup) -> Distance:
def detection_range(self, group: TheaterGroup) -> Distance:
return self._max_range_of_type(group, "detection_range")
def max_threat_range(self) -> Distance:
@@ -298,7 +170,7 @@ class TheaterGroundObject(MissionTarget):
max(self.threat_range(g) for g in self.groups) if self.groups else meters(0)
)
def threat_range(self, group: GroundGroup, radar_only: bool = False) -> Distance:
def threat_range(self, group: TheaterGroup, radar_only: bool = False) -> Distance:
return self._max_range_of_type(group, "threat_range")
@property
@@ -315,7 +187,7 @@ class TheaterGroundObject(MissionTarget):
return False
@property
def strike_targets(self) -> list[GroundUnit]:
def strike_targets(self) -> list[TheaterUnit]:
return [unit for unit in self.units if unit.alive]
@property
@@ -543,13 +415,15 @@ class SamGroundObject(IadsGroundObject):
def might_have_aa(self) -> bool:
return True
def threat_range(self, group: GroundGroup, radar_only: bool = False) -> Distance:
def threat_range(self, group: TheaterGroup, radar_only: bool = False) -> Distance:
max_non_radar = meters(0)
live_trs = set()
max_telar_range = meters(0)
launchers = set()
for unit in group.units:
unit_type = vehicle_map[unit.type]
if not unit.alive or not issubclass(unit.type, VehicleType):
continue
unit_type = unit.type
if unit_type in TRACK_RADARS:
live_trs.add(unit_type)
elif unit_type in TELARS:

View File

@@ -0,0 +1,167 @@
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Optional, Any, TYPE_CHECKING, Type
from dcs.triggers import TriggerZone
from dcs.unittype import VehicleType, ShipType, StaticType
from game.dcs.groundunittype import GroundUnitType
from game.dcs.shipunittype import ShipUnitType
from game.dcs.unittype import UnitType
from dcs.unittype import UnitType as DcsUnitType
from game.point_with_heading import PointWithHeading
from game.utils import Heading
if TYPE_CHECKING:
from game.layout.layout import LayoutUnit, GroupLayout
from game.theater import TheaterGroundObject
@dataclass
class TheaterUnit:
"""Representation of a single Unit in the Game"""
# Every Unit has a unique ID generated from the game
id: int
# The name of the Unit. Not required to be unique
name: str
# DCS UniType of the unit
type: Type[DcsUnitType]
# Position and orientation of the Unit
position: PointWithHeading
# The parent ground object
ground_object: TheaterGroundObject
# State of the unit, dead or alive
alive: bool = True
@staticmethod
def from_template(
id: int, dcs_type: Type[DcsUnitType], t: LayoutUnit, go: TheaterGroundObject
) -> TheaterUnit:
return TheaterUnit(
id,
t.name,
dcs_type,
PointWithHeading.from_point(t.position, Heading.from_degrees(t.heading)),
go,
)
@property
def unit_type(self) -> Optional[UnitType[Any]]:
if issubclass(self.type, VehicleType):
return next(GroundUnitType.for_dcs_type(self.type))
elif issubclass(self.type, ShipType):
return next(ShipUnitType.for_dcs_type(self.type))
# None for not available StaticTypes
return None
def kill(self) -> None:
self.alive = False
@property
def unit_name(self) -> str:
return f"{str(self.id).zfill(4)} | {self.name}"
@property
def display_name(self) -> str:
dead_label = " [DEAD]" if not self.alive else ""
unit_label = self.unit_type or self.type.name or self.name
return f"{str(self.id).zfill(4)} | {unit_label}{dead_label}"
@property
def short_name(self) -> str:
dead_label = " [DEAD]" if not self.alive else ""
return f"<b>{self.type.id[0:18]}</b> {dead_label}"
@property
def is_static(self) -> bool:
return issubclass(self.type, StaticType)
@property
def is_vehicle(self) -> bool:
return issubclass(self.type, VehicleType)
@property
def is_ship(self) -> bool:
return issubclass(self.type, ShipType)
@property
def icon(self) -> str:
return self.type.id
@property
def repairable(self) -> bool:
# Only let units with UnitType be repairable as we just have prices for them
return self.unit_type is not None
class SceneryUnit(TheaterUnit):
"""Special TheaterUnit for handling scenery ground objects"""
# Scenery Objects are identified by a special trigger zone
zone: TriggerZone
@property
def display_name(self) -> str:
dead_label = " [DEAD]" if not self.alive else ""
return f"{str(self.id).zfill(4)} | {self.name}{dead_label}"
@property
def short_name(self) -> str:
dead_label = " [DEAD]" if not self.alive else ""
return f"<b>{self.name[0:18]}</b> {dead_label}"
@property
def icon(self) -> str:
return "missing"
@property
def repairable(self) -> bool:
return False
@dataclass
class TheaterGroup:
"""Logical group for multiple TheaterUnits at a specific position"""
# Every Theater Group has a unique ID generated from the game
id: int # Unique ID
# The name of the Group. Not required to be unique
name: str
# Position and orientation of the Group
position: PointWithHeading
# All TheaterUnits within the group
units: list[TheaterUnit]
# The parent ground object
ground_object: TheaterGroundObject
@staticmethod
def from_template(
id: int,
g: GroupLayout,
go: TheaterGroundObject,
unit_type: Type[DcsUnitType],
unit_count: int,
) -> TheaterGroup:
return TheaterGroup(
id,
g.name,
PointWithHeading.from_point(go.position, go.heading),
g.generate_units(go, unit_type, unit_count),
go,
)
@property
def group_name(self) -> str:
return f"{str(self.id).zfill(4)} | {self.name}"
@property
def unit_count(self) -> int:
return len(self.units)
@property
def alive_units(self) -> int:
return sum([unit.alive for unit in self.units])

View File

@@ -12,9 +12,9 @@ from dcs.unitgroup import FlyingGroup, VehicleGroup, ShipGroup
from game.dcs.groundunittype import GroundUnitType
from game.squadrons import Pilot
from game.theater import Airfield, ControlPoint, GroundUnit
from game.theater import Airfield, ControlPoint, TheaterUnit
from game.ato.flight import Flight
from game.theater.theatergroundobject import SceneryGroundUnit
from game.theater.theatergroup import SceneryUnit
if TYPE_CHECKING:
from game.transfers import CargoShip, Convoy, TransferOrder
@@ -33,14 +33,14 @@ class FrontLineUnit:
@dataclass(frozen=True)
class GroundObjectMapping:
ground_unit: GroundUnit
class TheaterUnitMapping:
theater_unit: TheaterUnit
dcs_unit: Unit
@dataclass(frozen=True)
class SceneryObjectMapping:
ground_unit: GroundUnit
ground_unit: TheaterUnit
trigger_zone: TriggerZone
@@ -61,7 +61,7 @@ class UnitMap:
self.aircraft: Dict[str, FlyingUnit] = {}
self.airfields: Dict[str, Airfield] = {}
self.front_line_units: Dict[str, FrontLineUnit] = {}
self.ground_objects: Dict[str, GroundObjectMapping] = {}
self.theater_objects: Dict[str, TheaterUnitMapping] = {}
self.scenery_objects: Dict[str, SceneryObjectMapping] = {}
self.convoys: Dict[str, ConvoyUnit] = {}
self.cargo_ships: Dict[str, CargoShip] = {}
@@ -103,18 +103,18 @@ class UnitMap:
def front_line_unit(self, name: str) -> Optional[FrontLineUnit]:
return self.front_line_units.get(name, None)
def add_ground_object_mapping(
self, ground_unit: GroundUnit, dcs_unit: Unit
def add_theater_unit_mapping(
self, theater_unit: TheaterUnit, dcs_unit: Unit
) -> None:
# Deaths for units at TGOs are recorded in the corresponding GroundUnit within
# the GroundGroup, so we have to match the dcs unit with the liberation unit
name = str(dcs_unit.name)
if name in self.ground_objects:
if name in self.theater_objects:
raise RuntimeError(f"Duplicate TGO unit: {name}")
self.ground_objects[name] = GroundObjectMapping(ground_unit, dcs_unit)
self.theater_objects[name] = TheaterUnitMapping(theater_unit, dcs_unit)
def ground_object(self, name: str) -> Optional[GroundObjectMapping]:
return self.ground_objects.get(name, None)
def theater_units(self, name: str) -> Optional[TheaterUnitMapping]:
return self.theater_objects.get(name, None)
def add_convoy_units(self, group: VehicleGroup, convoy: Convoy) -> None:
for unit, unit_type in zip(group.units, convoy.iter_units()):
@@ -170,9 +170,7 @@ class UnitMap:
def airlift_unit(self, name: str) -> Optional[AirliftUnits]:
return self.airlifts.get(name, None)
def add_scenery(
self, scenery_unit: SceneryGroundUnit, trigger_zone: TriggerZone
) -> None:
def add_scenery(self, scenery_unit: SceneryUnit, trigger_zone: TriggerZone) -> None:
name = str(trigger_zone.name)
if name in self.scenery_objects:
raise RuntimeError(f"Duplicate scenery object {name} (TriggerZone)")