mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Improve Layout System and adopt to review
- adopted to review comments - removed the role from layouts - reworked the Groups within the layouts - added more documentation - rebased to latest changes
This commit is contained in:
@@ -2,21 +2,20 @@ 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.data.groups import GroupTask
|
||||
from game.armedforces.forcegroup import ForceGroup
|
||||
from game.layout import LAYOUTS
|
||||
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]]
|
||||
forces: list[ForceGroup]
|
||||
|
||||
def __init__(self, faction: Faction):
|
||||
with logged_duration(f"Loading armed forces for {faction.name}"):
|
||||
@@ -24,50 +23,36 @@ class ArmedForces:
|
||||
|
||||
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
|
||||
# Check if a force group with the same units exists
|
||||
for force_group in self.forces:
|
||||
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]
|
||||
self.forces.append(new_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 = {}
|
||||
"""Initialize the ArmedForces for the given faction.
|
||||
This will create a ForceGroup for each generic Layout and PresetGroup"""
|
||||
|
||||
preset_layouts = [
|
||||
layout
|
||||
for preset_group in faction.preset_groups
|
||||
for layout in preset_group.layouts
|
||||
]
|
||||
# Initialize with preset_groups from the faction
|
||||
self.forces = [preset_group for preset_group in faction.preset_groups]
|
||||
|
||||
# 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):
|
||||
# Generate ForceGroup for all generic layouts by iterating over
|
||||
# all layouts which are usable by the given faction.
|
||||
for layout in LAYOUTS.layouts:
|
||||
if layout.generic 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
|
||||
for force_group in self.forces:
|
||||
if group_task in force_group.tasks:
|
||||
yield force_group
|
||||
|
||||
def groups_for_tasks(self, tasks: list[GroupTask]) -> list[ForceGroup]:
|
||||
groups = []
|
||||
|
||||
@@ -9,18 +9,20 @@ 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.groups import GroupTask
|
||||
from game.data.radar_db import UNITS_WITH_RADAR
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.dcs.helpers import static_type_from_name
|
||||
from game.dcs.shipunittype import ShipUnitType
|
||||
from game.dcs.unittype import UnitType
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from game.layout.layout import TheaterLayout, AntiAirLayout, GroupLayout
|
||||
from game.layout.layout import TgoLayout, AntiAirLayout, TgoLayoutGroup
|
||||
from dcs.unittype import UnitType as DcsUnitType, VehicleType, ShipType, StaticType
|
||||
|
||||
from game.theater.theatergroup import TheaterGroup
|
||||
|
||||
from game.layout import LAYOUTS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.factions.faction import Faction
|
||||
@@ -29,25 +31,40 @@ if TYPE_CHECKING:
|
||||
|
||||
@dataclass
|
||||
class ForceGroup:
|
||||
"""A logical group of multiple units and layouts which have a specific tasking"""
|
||||
"""A logical group of multiple units and layouts which have a specific tasking.
|
||||
|
||||
ForceGroups will be generated during game and coalition initialization based on
|
||||
generic layouts and preset forcegroups.
|
||||
|
||||
Every ForceGroup must have at least one unit, one task and one layout.
|
||||
|
||||
A preset ForceGroup can for example be a S-300 SAM Battery which used many
|
||||
different unit types which all together handle a specific tasking (AirDefense)
|
||||
For this example the ForceGroup would consist of SR, TR, LN and so on next to
|
||||
statics. This group also has the Tasking LORAD and can have multiple (at least one)
|
||||
layouts which will be used to generate the actual DCS Group from it.
|
||||
"""
|
||||
|
||||
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)
|
||||
layouts: list[TgoLayout] = 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"""
|
||||
def for_layout(layout: TgoLayout, faction: Faction) -> ForceGroup:
|
||||
"""Create a ForceGroup from the given TgoLayout which is usable by the faction
|
||||
|
||||
This will iterate through all possible TgoLayoutGroups and check if the
|
||||
unit_types are accessible by the faction. All accessible units will be added to
|
||||
the force group
|
||||
"""
|
||||
units: set[UnitType[Any]] = set()
|
||||
statics: set[Type[DcsUnitType]] = set()
|
||||
for group in layout.groups:
|
||||
for group in layout.all_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)))
|
||||
@@ -57,10 +74,9 @@ class ForceGroup:
|
||||
statics.add(unit_type)
|
||||
|
||||
return ForceGroup(
|
||||
f"{layout.role.value}: {', '.join([t.description for t in layout.tasks])}",
|
||||
", ".join([t.description for t in layout.tasks]),
|
||||
list(units),
|
||||
list(statics),
|
||||
layout.role,
|
||||
layout.tasks,
|
||||
[layout],
|
||||
)
|
||||
@@ -80,33 +96,38 @@ class ForceGroup:
|
||||
or type in self.statics
|
||||
)
|
||||
|
||||
def dcs_unit_types_for_group(self, group: GroupLayout) -> list[Type[DcsUnitType]]:
|
||||
"""TODO Description"""
|
||||
def dcs_unit_types_for_group(
|
||||
self, group: TgoLayoutGroup
|
||||
) -> list[Type[DcsUnitType]]:
|
||||
"""Return all available DCS Unit Types which can be used in the given
|
||||
TgoLayoutGroup"""
|
||||
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:
|
||||
if accessible_unit.unit_class in group.fallback_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]]:
|
||||
def unit_types_for_group(self, group: TgoLayoutGroup) -> 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]]:
|
||||
def statics_for_group(self, group: TgoLayoutGroup) -> 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"""
|
||||
def random_dcs_unit_type_for_group(
|
||||
self, group: TgoLayoutGroup
|
||||
) -> Type[DcsUnitType]:
|
||||
"""Return random DCS Unit Type which can be used in the given TgoLayoutGroup"""
|
||||
return random.choice(self.dcs_unit_types_for_group(group))
|
||||
|
||||
def update_group(self, new_group: ForceGroup) -> None:
|
||||
@@ -130,7 +151,7 @@ class ForceGroup:
|
||||
|
||||
def create_ground_object_for_layout(
|
||||
self,
|
||||
layout: TheaterLayout,
|
||||
layout: TgoLayout,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
control_point: ControlPoint,
|
||||
@@ -139,25 +160,31 @@ class ForceGroup:
|
||||
"""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)
|
||||
for group_name, groups in layout.groups.items():
|
||||
for group in 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}"
|
||||
)
|
||||
tgo_group_name = f"{name} ({group_name})"
|
||||
self.create_theater_group_for_tgo(
|
||||
go, group, tgo_group_name, game, unit_type
|
||||
)
|
||||
|
||||
return go
|
||||
|
||||
def create_theater_group_for_tgo(
|
||||
self,
|
||||
ground_object: TheaterGroundObject,
|
||||
group: GroupLayout,
|
||||
name: str,
|
||||
group: TgoLayoutGroup,
|
||||
group_name: str,
|
||||
game: Game,
|
||||
unit_type: Type[DcsUnitType],
|
||||
unit_count: Optional[int] = None,
|
||||
@@ -165,25 +192,29 @@ class ForceGroup:
|
||||
"""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)
|
||||
unit_count = group.group_size
|
||||
# Generate Units
|
||||
units = group.generate_units(ground_object, unit_type, unit_count)
|
||||
# Get or create the TheaterGroup
|
||||
ground_group = ground_object.group_by_name(group_name)
|
||||
if ground_group is not None:
|
||||
# TheaterGroup with this name exists already. Extend it
|
||||
ground_group.units.extend(units)
|
||||
else:
|
||||
# TheaterGroup with the name was not created yet
|
||||
ground_object.groups.append(
|
||||
TheaterGroup.from_template(
|
||||
game.next_group_id(),
|
||||
group_name,
|
||||
units,
|
||||
ground_object,
|
||||
unit_type,
|
||||
unit_count,
|
||||
)
|
||||
)
|
||||
|
||||
# Assign UniqueID, name and align relative to ground_object
|
||||
for u_id, unit in enumerate(units):
|
||||
for unit in 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(
|
||||
@@ -209,42 +240,46 @@ class ForceGroup:
|
||||
|
||||
@classmethod
|
||||
def _load_all(cls) -> None:
|
||||
for file in Path("resources/units/groups").glob("*.yaml"):
|
||||
for file in Path("resources/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"))
|
||||
name = data["name"]
|
||||
|
||||
group_tasks = [GroupTask.by_description(n) for n in data.get("tasks", [])]
|
||||
group_tasks = [GroupTask.by_description(n) for n in data.get("tasks")]
|
||||
if not group_tasks:
|
||||
logging.error(f"ForceGroup {name} has no valid tasking")
|
||||
continue
|
||||
|
||||
units = [UnitType.named(unit) for unit in data.get("units", [])]
|
||||
units = [UnitType.named(unit) for unit in data.get("units")]
|
||||
if not units:
|
||||
logging.error(f"ForceGroup {name} has no valid units")
|
||||
continue
|
||||
|
||||
statics = []
|
||||
for static in data.get("statics", []):
|
||||
static_type = db.static_type_from_name(static)
|
||||
static_type = 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")]
|
||||
layouts = [LAYOUTS.by_name(n) for n in data.get("layouts")]
|
||||
if not layouts:
|
||||
logging.error(f"ForceGroup {name} has no valid layouts")
|
||||
continue
|
||||
|
||||
force_group = ForceGroup(
|
||||
name=data.get("name"),
|
||||
name=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
|
||||
|
||||
@@ -11,6 +11,7 @@ from pydantic.dataclasses import dataclass
|
||||
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.theater import LatLon
|
||||
from game.theater.theatergroup import TheaterUnit
|
||||
from game.utils import Distance, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -94,7 +95,7 @@ class FlightWaypoint(BaseFlightWaypoint):
|
||||
# having three names. A short and long form is enough.
|
||||
description: str = ""
|
||||
|
||||
targets: Sequence[MissionTarget | Unit] = []
|
||||
targets: Sequence[MissionTarget | TheaterUnit] = []
|
||||
obj_name: str = ""
|
||||
pretty_name: str = ""
|
||||
only_for_player: bool = False
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .faction import Faction
|
||||
from .faction_loader import FactionLoader
|
||||
from .factionloader import FactionLoader
|
||||
|
||||
FACTIONS = FactionLoader()
|
||||
|
||||
@@ -150,7 +150,7 @@ class Faction:
|
||||
for unit in preset_group.units
|
||||
),
|
||||
)
|
||||
return list(all_units)
|
||||
return list(set(all_units))
|
||||
|
||||
@property
|
||||
def air_defenses(self) -> list[str]:
|
||||
@@ -158,7 +158,11 @@ class Faction:
|
||||
# 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]
|
||||
[
|
||||
pg.name
|
||||
for pg in self.preset_groups
|
||||
if any(task.role == GroupRole.AIR_DEFENSE for task in pg.tasks)
|
||||
]
|
||||
)
|
||||
return sorted(air_defenses)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from layout import TheaterLayout
|
||||
from game.layout.layoutloader import LayoutLoader
|
||||
from .layout import TgoLayout, TgoLayoutGroup
|
||||
from .layoutloader import LayoutLoader
|
||||
|
||||
LAYOUTS = LayoutLoader()
|
||||
LAYOUTS = LayoutLoader()
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Type
|
||||
from typing import TYPE_CHECKING, Iterator, Type
|
||||
|
||||
from dcs import Point
|
||||
from dcs.unit import Unit
|
||||
@@ -50,98 +51,121 @@ class LayoutUnit:
|
||||
"""Creates a LayoutUnit from a DCS Unit"""
|
||||
return LayoutUnit(
|
||||
unit.name,
|
||||
Point(int(unit.position.x), int(unit.position.y)),
|
||||
unit.position,
|
||||
int(unit.heading),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroupLayout:
|
||||
"""The Layout of a TheaterGroup"""
|
||||
class TgoLayoutGroup:
|
||||
"""The layout of a single type of unit within the TgoLayout
|
||||
|
||||
Each DCS group that is spawned in the mission is composed of one or more
|
||||
TgoLayoutGroup. Each TgoLayoutGroup will generate only a single type of unit.
|
||||
|
||||
The merging of multiple TgoLayoutGroups to a single DCS group is defined in the
|
||||
TgoLayout with a dict which uses the Dcs group name as key and the corresponding
|
||||
TgoLayoutGroups as values.
|
||||
|
||||
Each TgoLayoutGroup will be filled with a single type of unit when generated. The
|
||||
types compatible with the position can either be specified precisely (with
|
||||
unit_types) or generically (with unit_classes). If neither list specifies units
|
||||
that can be fulfilled by the faction, fallback_classes will be used. This allows
|
||||
the early-warning radar template, which prefers units that are defined as early
|
||||
warning radars like the 55G6, but to fall back to any radar usable by the faction
|
||||
if EWRs are not available.
|
||||
|
||||
A TgoLayoutGroup may be optional. Factions or ForceGroups that are not able to
|
||||
provide an actual unit for the TgoLayoutGroup will still be able to use the layout;
|
||||
the optional TgoLayoutGroup will be omitted.
|
||||
"""
|
||||
|
||||
name: str
|
||||
units: list[LayoutUnit]
|
||||
layout_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
|
||||
# Define the amount of units to be created. This can be a fixed int or a random
|
||||
# choice from a range of two ints. If the list is empty it will use the whole group
|
||||
# size / all available LayoutUnits
|
||||
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)
|
||||
fallback_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"""
|
||||
"""Determine the possible dcs unit types for the TgoLayoutGroup and the given faction"""
|
||||
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:
|
||||
if accessible_unit.unit_class in self.fallback_classes:
|
||||
alternative_types.append(accessible_unit.dcs_unit_type)
|
||||
|
||||
if not unit_types and not alternative_types and not self.optional:
|
||||
raise LayoutException
|
||||
raise LayoutException(f"{self.name} not usable by faction {faction.name}")
|
||||
|
||||
return unit_types or alternative_types
|
||||
|
||||
@property
|
||||
def unit_counter(self) -> int:
|
||||
"""TODO Documentation"""
|
||||
default = len(self.units)
|
||||
def group_size(self) -> int:
|
||||
"""The amount of units to be generated. If unit_count is defined in the layout this will be randomized accordingly. Otherwise this will be the maximum size."""
|
||||
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
|
||||
return self.unit_count[0]
|
||||
return random.choice(range(min(self.unit_count), max(self.unit_count)))
|
||||
return self.max_size
|
||||
|
||||
@property
|
||||
def max_size(self) -> int:
|
||||
return len(self.units)
|
||||
return len(self.layout_units)
|
||||
|
||||
def generate_units(
|
||||
self, go: TheaterGroundObject, unit_type: Type[DcsUnitType], amount: int
|
||||
) -> list[TheaterUnit]:
|
||||
"""TODO Documentation"""
|
||||
"""Generate units of the given unit type and amount for the TgoLayoutGroup"""
|
||||
return [
|
||||
TheaterUnit.from_template(i, unit_type, self.units[i], go)
|
||||
TheaterUnit.from_template(i, unit_type, self.layout_units[i], go)
|
||||
for i in range(amount)
|
||||
]
|
||||
|
||||
|
||||
class TheaterLayout:
|
||||
"""TODO Documentation"""
|
||||
class TgoLayout:
|
||||
"""TgoLayout defines how a TheaterGroundObject will be generated from a ForceGroup. This defines the positioning, orientation, type and amount of the actual units
|
||||
|
||||
def __init__(self, name: str, role: GroupRole, description: str = "") -> None:
|
||||
Each TgoLayout is defined in resources/layouts with a .yaml file which has all the
|
||||
information about the Layout next to a .miz file which gives information about the
|
||||
actual position (x, y) and orientation (heading) of the units. The layout file also
|
||||
defines the structure of the DCS group (or groups) that will be spawned in the
|
||||
mission. Complex groups like SAMs protected by point-defense require specific
|
||||
grouping when used with plugins like Skynet. One group would define the main
|
||||
battery (the search and track radars, launchers, C2 units, etc), another would
|
||||
define PD units, and others could define SHORADs or resupply units.
|
||||
|
||||
Each group (representing a DCS group) is further divided into TgoLayoutGroups. As
|
||||
a TgoLayoutGroup only represents a single dcs unit type the logical dcs group of multiple unit types will be created with the usage of a dict which has the DCS Group name as key and a list of TgoLayoutGroups which will be merged into this single dcs group.
|
||||
|
||||
As the TgoLayout will be used to create a TheaterGroundObject for a ForceGroup,
|
||||
specialized classes inherit from this base class. For example there is a special
|
||||
AiDefenseLayout which will be used to create the SamGroundObject from it.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, description: str = "") -> None:
|
||||
self.name = name
|
||||
self.role = role
|
||||
self.description = description
|
||||
self.tasks: list[GroupTask] = [] # The supported tasks
|
||||
self.groups: list[GroupLayout] = []
|
||||
self.tasks: list[GroupTask] = [] # The supported
|
||||
|
||||
# If the template is generic it will be used the generate the general
|
||||
# UnitGroups during faction initialization. Generic Groups allow to be mixed
|
||||
# Mapping of group name and LayoutGroups for a specific TgoGroup
|
||||
# A Group can have multiple TgoLayoutGroups which get merged together
|
||||
self.groups: dict[str, list[TgoLayoutGroup]] = defaultdict(list)
|
||||
|
||||
# A generic layout will be used to create generic ForceGroups during the
|
||||
# campaign initialization. For each generic layout a new Group will be created.
|
||||
self.generic: bool = False
|
||||
|
||||
def usable_by_faction(self, faction: Faction) -> bool:
|
||||
@@ -156,7 +180,7 @@ class TheaterLayout:
|
||||
try:
|
||||
return all(
|
||||
len(group.possible_types_for_faction(faction)) > 0
|
||||
for group in self.groups
|
||||
for group in self.all_groups
|
||||
if not group.optional
|
||||
)
|
||||
except LayoutException:
|
||||
@@ -168,22 +192,20 @@ class TheaterLayout:
|
||||
position: PointWithHeading,
|
||||
control_point: ControlPoint,
|
||||
) -> TheaterGroundObject:
|
||||
"""TODO Documentation"""
|
||||
"""Create the TheaterGroundObject for the TgoLayout
|
||||
|
||||
This function has to be implemented by the inheriting class to create
|
||||
a specific TGO like SamGroundObject or BuildingGroundObject
|
||||
"""
|
||||
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])
|
||||
def all_groups(self) -> Iterator[TgoLayoutGroup]:
|
||||
for groups in self.groups.values():
|
||||
yield from groups
|
||||
|
||||
|
||||
class AntiAirLayout(TheaterLayout):
|
||||
class AntiAirLayout(TgoLayout):
|
||||
def create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
@@ -200,7 +222,7 @@ class AntiAirLayout(TheaterLayout):
|
||||
)
|
||||
|
||||
|
||||
class BuildingLayout(TheaterLayout):
|
||||
class BuildingLayout(TgoLayout):
|
||||
def create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
@@ -224,7 +246,7 @@ class BuildingLayout(TheaterLayout):
|
||||
raise RuntimeError(f"Building Template {self.name} has no building category")
|
||||
|
||||
|
||||
class NavalLayout(TheaterLayout):
|
||||
class NavalLayout(TgoLayout):
|
||||
def create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
@@ -240,7 +262,7 @@ class NavalLayout(TheaterLayout):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DefensesLayout(TheaterLayout):
|
||||
class DefensesLayout(TgoLayout):
|
||||
def create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
@@ -258,7 +280,7 @@ class DefensesLayout(TheaterLayout):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class GroundForceLayout(TheaterLayout):
|
||||
class GroundForceLayout(TgoLayout):
|
||||
def create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
@@ -13,10 +14,10 @@ from dcs import Point
|
||||
from dcs.unitgroup import StaticGroup
|
||||
|
||||
from game import persistency
|
||||
from game.data.groups import GroupRole, GroupTask
|
||||
from game.data.groups import GroupRole
|
||||
from game.layout.layout import (
|
||||
TheaterLayout,
|
||||
GroupLayout,
|
||||
TgoLayout,
|
||||
TgoLayoutGroup,
|
||||
LayoutUnit,
|
||||
AntiAirLayout,
|
||||
BuildingLayout,
|
||||
@@ -24,7 +25,7 @@ from game.layout.layout import (
|
||||
GroundForceLayout,
|
||||
DefensesLayout,
|
||||
)
|
||||
from game.layout.layoutmapping import GroupLayoutMapping, LayoutMapping
|
||||
from game.layout.layoutmapping import LayoutMapping
|
||||
from game.profiling import logged_duration
|
||||
from game.version import VERSION
|
||||
|
||||
@@ -41,21 +42,21 @@ TEMPLATE_TYPES = {
|
||||
|
||||
|
||||
class LayoutLoader:
|
||||
# list of layouts per category. e.g. AA or similar
|
||||
_templates: dict[str, TheaterLayout] = {}
|
||||
# Map of all available layouts indexed by name
|
||||
_layouts: dict[str, TgoLayout] = {}
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._templates = {}
|
||||
self._layouts = {}
|
||||
|
||||
def initialize(self) -> None:
|
||||
if not self._templates:
|
||||
if not self._layouts:
|
||||
with logged_duration("Loading layouts"):
|
||||
self.load_templates()
|
||||
|
||||
@property
|
||||
def layouts(self) -> Iterator[TheaterLayout]:
|
||||
def layouts(self) -> Iterator[TgoLayout]:
|
||||
self.initialize()
|
||||
yield from self._templates.values()
|
||||
yield from self._layouts.values()
|
||||
|
||||
def load_templates(self) -> None:
|
||||
"""This will load all pre-loaded layouts from a pickle file.
|
||||
@@ -66,60 +67,43 @@ class LayoutLoader:
|
||||
# Load from pickle if existing
|
||||
with file.open("rb") as f:
|
||||
try:
|
||||
version, self._templates = pickle.load(f)
|
||||
version, self._layouts = 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.")
|
||||
logging.exception(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]] = {}
|
||||
self._layouts = {}
|
||||
mappings: dict[str, list[LayoutMapping]] = defaultdict(list)
|
||||
with logged_duration("Parsing mapping yamls"):
|
||||
for file in Path(TEMPLATE_DIR).rglob("*.yaml"):
|
||||
if not file.is_file():
|
||||
continue
|
||||
raise RuntimeError(f"{file.name} is not a file")
|
||||
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]
|
||||
mappings[template_map.layout_file].append(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)
|
||||
exe.map(self._load_from_miz, mappings.keys(), mappings.values())
|
||||
|
||||
logging.info(f"Imported {len(self._templates)} layouts")
|
||||
logging.info(f"Imported {len(self._layouts)} layouts")
|
||||
self._dump_templates()
|
||||
|
||||
def _dump_templates(self) -> None:
|
||||
file = Path(persistency.base_path()) / TEMPLATE_DUMP
|
||||
dump = (VERSION, self._templates)
|
||||
dump = (VERSION, self._layouts)
|
||||
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()
|
||||
@@ -130,74 +114,68 @@ class LayoutLoader:
|
||||
# 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,
|
||||
for mapping in mappings:
|
||||
# Find the group from the mapping in any coalition
|
||||
for country in itertools.chain(
|
||||
temp_mis.coalition["red"].countries.values(),
|
||||
temp_mis.coalition["blue"].countries.values(),
|
||||
):
|
||||
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
|
||||
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,
|
||||
):
|
||||
|
||||
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,
|
||||
try:
|
||||
group_name, group_mapping = mapping.group_for_name(
|
||||
dcs_group.name
|
||||
)
|
||||
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)
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
def by_name(self, template_name: str) -> Iterator[TheaterLayout]:
|
||||
for template in self.layouts:
|
||||
if template.name == template_name:
|
||||
yield template
|
||||
if not isinstance(dcs_group, StaticGroup) and max(
|
||||
group_mapping.unit_count
|
||||
) > len(dcs_group.units):
|
||||
logging.error(
|
||||
f"Incorrect unit_count found in Layout {mapping.name}-{group_mapping.name}"
|
||||
)
|
||||
|
||||
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
|
||||
layout = self._layouts.get(mapping.name, None)
|
||||
if layout is None:
|
||||
# Create a new template
|
||||
layout = TEMPLATE_TYPES[mapping.primary_role](
|
||||
mapping.name, mapping.description
|
||||
)
|
||||
layout.generic = mapping.generic
|
||||
layout.tasks = mapping.tasks
|
||||
self._layouts[layout.name] = layout
|
||||
|
||||
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
|
||||
for i, unit in enumerate(dcs_group.units):
|
||||
group_layout = None
|
||||
for group in layout.all_groups:
|
||||
if group.name == group_mapping.name:
|
||||
# We already have a layoutgroup for this dcs_group
|
||||
group_layout = group
|
||||
if not group_layout:
|
||||
group_layout = TgoLayoutGroup(
|
||||
group_mapping.name,
|
||||
[],
|
||||
group_mapping.unit_count,
|
||||
group_mapping.unit_types,
|
||||
group_mapping.unit_classes,
|
||||
group_mapping.fallback_classes,
|
||||
)
|
||||
group_layout.optional = group_mapping.optional
|
||||
# Add the group at the correct position
|
||||
layout.groups[group_name].append(group_layout)
|
||||
layout_unit = LayoutUnit.from_unit(unit)
|
||||
if i == 0 and layout.name not in template_position:
|
||||
template_position[layout.name] = unit.position
|
||||
layout_unit.position = (
|
||||
layout_unit.position - template_position[layout.name]
|
||||
)
|
||||
group_layout.layout_units.append(layout_unit)
|
||||
|
||||
def by_name(self, name: str) -> TgoLayout:
|
||||
self.initialize()
|
||||
return self._layouts[name]
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
|
||||
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
|
||||
from game.dcs.helpers import unit_type_from_name
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -21,10 +22,6 @@ class GroupLayoutMapping:
|
||||
# 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.
|
||||
@@ -36,31 +33,9 @@ class GroupLayoutMapping:
|
||||
# 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
|
||||
# Fallback Classes which are used when the unit_classes and unit_types do not fit any accessible unit from the faction. 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
|
||||
fallback_classes: list[UnitClass] = field(default_factory=list)
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d: dict[str, Any]) -> GroupLayoutMapping:
|
||||
@@ -70,27 +45,25 @@ class GroupLayoutMapping:
|
||||
unit_types = []
|
||||
if "unit_types" in d:
|
||||
for u in d["unit_types"]:
|
||||
unit_type = db.unit_type_from_name(u)
|
||||
unit_type = 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
|
||||
fallback_classes = (
|
||||
[UnitClass(u) for u in d["fallback_classes"]]
|
||||
if "fallback_classes" in d
|
||||
else []
|
||||
)
|
||||
return GroupLayoutMapping(
|
||||
d["name"],
|
||||
optional,
|
||||
statics,
|
||||
group,
|
||||
unit_count,
|
||||
unit_types,
|
||||
unit_classes,
|
||||
alternative_classes,
|
||||
fallback_classes,
|
||||
)
|
||||
|
||||
|
||||
@@ -105,40 +78,31 @@ class LayoutMapping:
|
||||
# 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]
|
||||
groups: dict[str, 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
|
||||
@property
|
||||
def primary_role(self) -> GroupRole:
|
||||
return self.tasks[0].role
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d: dict[str, Any], file_name: str) -> LayoutMapping:
|
||||
groups = [GroupLayoutMapping.from_dict(group) for group in d["groups"]]
|
||||
groups: dict[str, list[GroupLayoutMapping]] = defaultdict(list)
|
||||
for group in d["groups"]:
|
||||
for group_name, group_layouts in group.items():
|
||||
groups[group_name].extend(
|
||||
[
|
||||
GroupLayoutMapping.from_dict(group_layout)
|
||||
for group_layout in group_layouts
|
||||
]
|
||||
)
|
||||
|
||||
description = d["description"] if "description" in d else ""
|
||||
generic = d["generic"] if "generic" in d else False
|
||||
layout_file = (
|
||||
@@ -149,8 +113,14 @@ class LayoutMapping:
|
||||
d["name"],
|
||||
description,
|
||||
generic,
|
||||
GroupRole(d["role"]),
|
||||
tasks,
|
||||
groups,
|
||||
layout_file,
|
||||
)
|
||||
|
||||
def group_for_name(self, name: str) -> tuple[str, GroupLayoutMapping]:
|
||||
for group_name, group_mappings in self.groups.items():
|
||||
for group_mapping in group_mappings:
|
||||
if group_mapping.name == name or name in group_mapping.statics:
|
||||
return group_name, group_mapping
|
||||
raise KeyError
|
||||
|
||||
@@ -153,11 +153,6 @@ class ControlPointGroundObjectGenerator:
|
||||
self.generate_navy()
|
||||
return True
|
||||
|
||||
def generate_random_ground_object(
|
||||
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: ForceGroup, position: PointWithHeading
|
||||
) -> None:
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import itertools
|
||||
import logging
|
||||
from abc import ABC
|
||||
from typing import Iterator, List, TYPE_CHECKING
|
||||
from typing import Iterator, List, TYPE_CHECKING, Optional
|
||||
|
||||
from dcs.unittype import VehicleType
|
||||
from dcs.vehicles import vehicle_map
|
||||
@@ -205,6 +205,12 @@ class TheaterGroundObject(MissionTarget):
|
||||
def purchasable(self) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
def group_by_name(self, name: str) -> Optional[TheaterGroup]:
|
||||
for group in self.groups:
|
||||
if group.name == name:
|
||||
return group
|
||||
return None
|
||||
|
||||
|
||||
class BuildingGroundObject(TheaterGroundObject):
|
||||
def __init__(
|
||||
|
||||
@@ -16,7 +16,7 @@ from game.point_with_heading import PointWithHeading
|
||||
from game.utils import Heading
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.layout.layout import LayoutUnit, GroupLayout
|
||||
from game.layout.layout import LayoutUnit, TgoLayoutGroup
|
||||
from game.theater import TheaterGroundObject
|
||||
|
||||
|
||||
@@ -141,16 +141,17 @@ class TheaterGroup:
|
||||
@staticmethod
|
||||
def from_template(
|
||||
id: int,
|
||||
g: GroupLayout,
|
||||
name: str,
|
||||
units: list[TheaterUnit],
|
||||
go: TheaterGroundObject,
|
||||
unit_type: Type[DcsUnitType],
|
||||
unit_count: int,
|
||||
) -> TheaterGroup:
|
||||
return TheaterGroup(
|
||||
id,
|
||||
g.name,
|
||||
name,
|
||||
PointWithHeading.from_point(go.position, go.heading),
|
||||
g.generate_units(go, unit_type, unit_count),
|
||||
units,
|
||||
go,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user