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:
RndName
2022-02-15 17:02:52 +01:00
parent 2c17a9a52e
commit 54e24dff39
93 changed files with 1606 additions and 1710 deletions

View File

@@ -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 = []

View File

@@ -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

View File

@@ -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

View File

@@ -1,4 +1,4 @@
from .faction import Faction
from .faction_loader import FactionLoader
from .factionloader import FactionLoader
FACTIONS = FactionLoader()

View File

@@ -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)

View File

@@ -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()

View File

@@ -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,

View File

@@ -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]

View File

@@ -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

View File

@@ -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:

View File

@@ -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__(

View File

@@ -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,
)