Improve Layout loading and ForceGroup generation

- fix layout not preserving the correct group index
- fix ForceGroup generation merging preset_groups with generics
This commit is contained in:
RndName 2022-05-13 20:25:08 +02:00
parent 72682e4db3
commit 50b82f6383
No known key found for this signature in database
GPG Key ID: 5EF516FD9537F7C0
7 changed files with 115 additions and 84 deletions

View File

@ -35,12 +35,6 @@ class ArmedForces:
"""Initialize the ArmedForces for the given faction.
This will create a ForceGroup for each generic Layout and PresetGroup"""
# Initialize with preset_groups from the faction
self.forces = [
preset_group.initialize_for_faction(faction)
for preset_group in faction.preset_groups
]
# Generate ForceGroup for all generic layouts by iterating over
# all layouts which are usable by the given faction.
for layout in LAYOUTS.layouts:
@ -48,6 +42,10 @@ class ArmedForces:
# Creates a faction compatible GorceGroup
self.add_or_update_force_group(ForceGroup.for_layout(layout, faction))
# Add all preset groups afterwards to prevent them being merged with generics
for preset_group in faction.preset_groups:
self.forces.append(preset_group.initialize_for_faction(faction))
def groups_for_task(self, group_task: GroupTask) -> Iterator[ForceGroup]:
for force_group in self.forces:
if group_task in force_group.tasks:
@ -59,7 +57,7 @@ class ArmedForces:
for group in self.groups_for_task(task):
if group not in groups:
groups.append(group)
return groups
return sorted(groups, key=lambda g: g.name)
def random_group_for_task(self, group_task: GroupTask) -> Optional[ForceGroup]:
unit_groups = list(self.groups_for_task(group_task))

View File

@ -21,7 +21,7 @@ from game.theater.theatergroundobject import (
NavalGroundObject,
)
from game.layout import LAYOUTS
from game.layout.layout import TgoLayout, TgoLayoutGroup
from game.layout.layout import TgoLayout, TgoLayoutUnitGroup
from game.point_with_heading import PointWithHeading
from game.theater.theatergroup import IadsGroundGroup, IadsRole, TheaterGroup
from game.utils import escape_string_for_lua
@ -67,10 +67,10 @@ class ForceGroup:
"""
units: set[UnitType[Any]] = set()
statics: set[Type[DcsUnitType]] = set()
for group in layout.all_groups:
if group.optional and not group.fill:
for unit_group in layout.all_unit_groups:
if unit_group.optional and not unit_group.fill:
continue
for unit_type in group.possible_types_for_faction(faction):
for unit_type in unit_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):
@ -89,11 +89,11 @@ class ForceGroup:
def __str__(self) -> str:
return self.name
def has_unit_for_layout_group(self, group: TgoLayoutGroup) -> bool:
def has_unit_for_layout_group(self, unit_group: TgoLayoutUnitGroup) -> bool:
for unit in self.units:
if (
unit.dcs_unit_type in group.unit_types
or unit.unit_class in group.unit_classes
unit.dcs_unit_type in unit_group.unit_types
or unit.unit_class in unit_group.unit_classes
):
return True
return False
@ -102,9 +102,9 @@ class ForceGroup:
"""Initialize a ForceGroup for the given Faction.
This adds accessible units to LayoutGroups with the fill property"""
for layout in self.layouts:
for group in layout.all_groups:
if group.fill and not self.has_unit_for_layout_group(group):
for unit_type in group.possible_types_for_faction(faction):
for unit_group in layout.all_unit_groups:
if unit_group.fill and not self.has_unit_for_layout_group(unit_group):
for unit_type in unit_group.possible_types_for_faction(faction):
if issubclass(unit_type, VehicleType):
self.units.append(
next(GroundUnitType.for_dcs_type(unit_type))
@ -138,38 +138,44 @@ class ForceGroup:
)
def dcs_unit_types_for_group(
self, group: TgoLayoutGroup
self, unit_group: TgoLayoutUnitGroup
) -> 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)]
unit_types = [
t for t in unit_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:
if accessible_unit.unit_class in unit_group.unit_classes:
unit_types.append(accessible_unit.dcs_unit_type)
if accessible_unit.unit_class in group.fallback_classes:
if accessible_unit.unit_class in unit_group.fallback_classes:
alternative_types.append(accessible_unit.dcs_unit_type)
return unit_types or alternative_types
def unit_types_for_group(self, group: TgoLayoutGroup) -> Iterator[UnitType[Any]]:
for dcs_type in self.dcs_unit_types_for_group(group):
def unit_types_for_group(
self, unit_group: TgoLayoutUnitGroup
) -> Iterator[UnitType[Any]]:
for dcs_type in self.dcs_unit_types_for_group(unit_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: TgoLayoutGroup) -> Iterator[Type[DcsUnitType]]:
for dcs_type in self.dcs_unit_types_for_group(group):
def statics_for_group(
self, unit_group: TgoLayoutUnitGroup
) -> Iterator[Type[DcsUnitType]]:
for dcs_type in self.dcs_unit_types_for_group(unit_group):
if issubclass(dcs_type, StaticType):
yield dcs_type
def random_dcs_unit_type_for_group(
self, group: TgoLayoutGroup
self, unit_group: TgoLayoutUnitGroup
) -> 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))
return random.choice(self.dcs_unit_types_for_group(unit_group))
def merge_group(self, new_group: ForceGroup) -> None:
"""Merge the group with another similar group."""
@ -204,22 +210,22 @@ class ForceGroup:
"""Create a TheaterGroundObject for the given template"""
go = layout.create_ground_object(name, location, control_point)
# Generate all groups using the randomization if it defined
for group_name, groups in layout.groups.items():
for group in groups:
for tgo_group in layout.groups:
for unit_group in tgo_group.unit_groups:
# Choose a random unit_type for the group
try:
unit_type = self.random_dcs_unit_type_for_group(group)
unit_type = self.random_dcs_unit_type_for_group(unit_group)
except IndexError:
if group.optional:
if unit_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}"
f"No accessible unit for {self.name} - {unit_group.name}"
)
tgo_group_name = f"{name} ({group_name})"
tgo_group_name = f"{name} ({tgo_group.group_name})"
self.create_theater_group_for_tgo(
go, group, tgo_group_name, game, unit_type
go, unit_group, tgo_group_name, game, unit_type
)
return go
@ -227,7 +233,7 @@ class ForceGroup:
def create_theater_group_for_tgo(
self,
ground_object: TheaterGroundObject,
group: TgoLayoutGroup,
unit_group: TgoLayoutUnitGroup,
group_name: str,
game: Game,
unit_type: Type[DcsUnitType],
@ -237,12 +243,12 @@ class ForceGroup:
# Random UnitCounter if not forced
if unit_count is None:
# Choose a random group_size based on the layouts unit_count
unit_count = group.group_size
unit_count = unit_group.group_size
if unit_count == 0:
# No units to be created so dont create a theater group for them
return
# Generate Units
units = group.generate_units(ground_object, unit_type, unit_count)
units = unit_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:
@ -261,9 +267,9 @@ class ForceGroup:
):
# Recreate the TheaterGroup as IadsGroundGroup
ground_group = IadsGroundGroup.from_group(ground_group)
if group.sub_task is not None:
if unit_group.sub_task is not None:
# Use the special sub_task of the TheaterGroup
iads_task = group.sub_task
iads_task = unit_group.sub_task
else:
# Use the primary task of the ForceGroup
iads_task = self.tasks[0]

View File

@ -1,4 +1,4 @@
from .layout import TgoLayout, TgoLayoutGroup
from .layout import TgoLayout, TgoLayoutGroup, TgoLayoutUnitGroup
from .layoutloader import LayoutLoader
LAYOUTS = LayoutLoader()

View File

@ -60,6 +60,21 @@ class LayoutUnit:
@dataclass
class TgoLayoutGroup:
"""The layout of a group which will generate a DCS group later. The TgoLayoutGroup has one or many TgoLayoutUnitGroup which represents a set of unit of the same type. Therefore the TgoLayoutGroup is a logical grouping of different unit_types. A TgoLayout can have one or many TgoLayoutGroup"""
# The group name which will be used later as the DCS group name
group_name: str
# The index of the group within the TgoLayout. Used to preserve that the order of
# the groups generated match the order defined in the layout yaml.
group_index: int
# List of all connected TgoLayoutUnitGroup
unit_groups: list[TgoLayoutUnitGroup] = field(default_factory=list)
@dataclass
class TgoLayoutUnitGroup:
"""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
@ -95,6 +110,9 @@ class TgoLayoutGroup:
unit_classes: list[UnitClass] = field(default_factory=list)
fallback_classes: list[UnitClass] = field(default_factory=list)
# The index of the TgoLayoutGroup within the Layout
unit_index: int = field(default_factory=int)
# Allows a group to have a special SubTask (PointDefence for example)
sub_task: Optional[GroupTask] = None
@ -172,21 +190,13 @@ class TgoLayout:
self.description = description
self.tasks: list[GroupTask] = [] # The supported
# 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)
# All TgoGroups this layout has.
self.groups: list[TgoLayoutGroup] = []
# 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 add_layout_group(
self, name: str, group: TgoLayoutGroup, index: int = 0
) -> None:
"""Adds the layout group to the group dict at the given index"""
# If the index is greater than the actual len it will add it after the last item
self.groups[name].insert(min(len(self.groups[name]), index), group)
def usable_by_faction(self, faction: Faction) -> bool:
# Special handling for Buildings
if (
@ -199,7 +209,7 @@ class TgoLayout:
try:
return all(
len(group.possible_types_for_faction(faction)) > 0
for group in self.all_groups
for group in self.all_unit_groups
if not group.optional
)
except LayoutException:
@ -219,9 +229,9 @@ class TgoLayout:
raise NotImplementedError
@property
def all_groups(self) -> Iterator[TgoLayoutGroup]:
for groups in self.groups.values():
yield from groups
def all_unit_groups(self) -> Iterator[TgoLayoutUnitGroup]:
for group in self.groups:
yield from group.unit_groups
class AntiAirLayout(TgoLayout):

View File

@ -18,6 +18,7 @@ from game.data.groups import GroupRole
from game.layout.layout import (
TgoLayout,
TgoLayoutGroup,
TgoLayoutUnitGroup,
LayoutUnit,
AntiAirLayout,
BuildingLayout,
@ -29,10 +30,10 @@ from game.layout.layoutmapping import LayoutMapping
from game.profiling import logged_duration
from game.version import VERSION
TEMPLATE_DIR = "resources/layouts/"
TEMPLATE_DUMP = "Liberation/layouts.p"
LAYOUT_DIR = "resources/layouts/"
LAYOUT_DUMP = "Liberation/layouts.p"
TEMPLATE_TYPES = {
LAYOUT_TYPES = {
GroupRole.AIR_DEFENSE: AntiAirLayout,
GroupRole.BUILDING: BuildingLayout,
GroupRole.NAVAL: NavalLayout,
@ -62,7 +63,7 @@ class LayoutLoader:
"""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
file = Path(persistency.base_path()) / LAYOUT_DUMP
if file.is_file():
# Load from pickle if existing
with file.open("rb") as f:
@ -82,7 +83,7 @@ class LayoutLoader:
self._layouts = {}
mappings: dict[str, list[LayoutMapping]] = defaultdict(list)
with logged_duration("Parsing mapping yamls"):
for file in Path(TEMPLATE_DIR).rglob("*.yaml"):
for file in Path(LAYOUT_DIR).rglob("*.yaml"):
if not file.is_file():
raise RuntimeError(f"{file.name} is not a file")
with file.open("r", encoding="utf-8") as f:
@ -95,11 +96,17 @@ class LayoutLoader:
with ThreadPoolExecutor() as exe:
exe.map(self._load_from_miz, mappings.keys(), mappings.values())
# Sort al the LayoutGroups with the correct index
for layout in self._layouts.values():
layout.groups.sort(key=lambda g: g.group_index)
for group in layout.groups:
group.unit_groups.sort(key=lambda ug: ug.unit_index)
logging.info(f"Imported {len(self._layouts)} layouts")
self._dump_templates()
def _dump_templates(self) -> None:
file = Path(persistency.base_path()) / TEMPLATE_DUMP
file = Path(persistency.base_path()) / LAYOUT_DUMP
dump = (VERSION, self._layouts)
with file.open("wb") as fdata:
pickle.dump(dump, fdata)
@ -127,7 +134,7 @@ class LayoutLoader:
):
try:
g_id, group_name, group_mapping = mapping.group_for_name(
g_id, u_id, group_name, group_mapping = mapping.group_for_name(
dcs_group.name
)
except KeyError:
@ -143,40 +150,46 @@ class LayoutLoader:
layout = self._layouts.get(mapping.name, None)
if layout is None:
# Create a new template
layout = TEMPLATE_TYPES[mapping.primary_role](
layout = LAYOUT_TYPES[mapping.primary_role](
mapping.name, mapping.description
)
layout.generic = mapping.generic
layout.tasks = mapping.tasks
self._layouts[layout.name] = layout
for i, unit in enumerate(dcs_group.units):
group_layout = None
for group in layout.all_groups:
if group.name == group_mapping.name:
unit_group = None
for _unit_group in layout.all_unit_groups:
if _unit_group.name == group_mapping.name:
# We already have a layoutgroup for this dcs_group
group_layout = group
if not group_layout:
group_layout = TgoLayoutGroup(
unit_group = _unit_group
if not unit_group:
unit_group = TgoLayoutUnitGroup(
group_mapping.name,
[],
group_mapping.unit_count,
group_mapping.unit_types,
group_mapping.unit_classes,
group_mapping.fallback_classes,
u_id,
)
group_layout.optional = group_mapping.optional
group_layout.fill = group_mapping.fill
group_layout.sub_task = group_mapping.sub_task
# Add the group at the correct index
layout.add_layout_group(group_name, group_layout, g_id)
unit_group.optional = group_mapping.optional
unit_group.fill = group_mapping.fill
unit_group.sub_task = group_mapping.sub_task
tgo_group = None
for _tgo_group in layout.groups:
if _tgo_group.group_name == group_name:
tgo_group = _tgo_group
if tgo_group is None:
tgo_group = TgoLayoutGroup(group_name, g_id)
layout.groups.append(tgo_group)
tgo_group.unit_groups.append(unit_group)
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)
unit_group.layout_units.append(layout_unit)
def by_name(self, name: str) -> TgoLayout:
self.initialize()

View File

@ -128,9 +128,11 @@ class LayoutMapping:
layout_file,
)
def group_for_name(self, name: str) -> tuple[int, str, GroupLayoutMapping]:
def group_for_name(self, name: str) -> tuple[int, int, str, GroupLayoutMapping]:
g_id = 0
for group_name, group_mappings in self.groups.items():
for g_id, group_mapping in enumerate(group_mappings):
for u_id, group_mapping in enumerate(group_mappings):
if group_mapping.name == name or name in group_mapping.statics:
return g_id, group_name, group_mapping
return g_id, u_id, group_name, group_mapping
g_id += 1
raise KeyError

View File

@ -25,7 +25,7 @@ from game.data.groups import GroupRole, GroupTask
from game.layout.layout import (
LayoutException,
TgoLayout,
TgoLayoutGroup,
TgoLayoutUnitGroup,
)
from game.theater import TheaterGroundObject
from game.theater.theatergroundobject import (
@ -38,7 +38,7 @@ from qt_ui.uiconstants import EVENT_ICONS
@dataclass
class QTgoLayoutGroup:
layout: TgoLayoutGroup
layout: TgoLayoutUnitGroup
dcs_unit_type: Type[UnitType]
amount: int
unit_price: int
@ -63,7 +63,7 @@ class QTgoLayout:
class QTgoLayoutGroupRow(QWidget):
group_template_changed = Signal()
def __init__(self, force_group: ForceGroup, group: TgoLayoutGroup) -> None:
def __init__(self, force_group: ForceGroup, group: TgoLayoutUnitGroup) -> None:
super().__init__()
self.grid_layout = QGridLayout()
self.setLayout(self.grid_layout)
@ -170,8 +170,10 @@ class QGroundObjectTemplateLayout(QGroupBox):
self.layout_model.groups = defaultdict(list)
for id in range(self.template_grid.count()):
self.template_grid.itemAt(id).widget().deleteLater()
for group_name, groups in self.layout_model.layout.groups.items():
self.add_theater_group(group_name, self.layout_model.force_group, groups)
for group in self.layout_model.layout.groups:
self.add_theater_group(
group.group_name, self.layout_model.force_group, group.unit_groups
)
self.group_template_changed()
@property
@ -183,7 +185,7 @@ class QGroundObjectTemplateLayout(QGroupBox):
return self.cost <= self.game.blue.budget
def add_theater_group(
self, group_name: str, force_group: ForceGroup, groups: list[TgoLayoutGroup]
self, group_name: str, force_group: ForceGroup, groups: list[TgoLayoutUnitGroup]
) -> None:
group_box = QGroupBox(group_name)
vbox_layout = QVBoxLayout()