diff --git a/game/armedforces/armedforces.py b/game/armedforces/armedforces.py index ee19926a..b8d34a61 100644 --- a/game/armedforces/armedforces.py +++ b/game/armedforces/armedforces.py @@ -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)) diff --git a/game/armedforces/forcegroup.py b/game/armedforces/forcegroup.py index 14701bd9..c0f7bf59 100644 --- a/game/armedforces/forcegroup.py +++ b/game/armedforces/forcegroup.py @@ -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] diff --git a/game/layout/__init__.py b/game/layout/__init__.py index 3131c6b9..a458aa4d 100644 --- a/game/layout/__init__.py +++ b/game/layout/__init__.py @@ -1,4 +1,4 @@ -from .layout import TgoLayout, TgoLayoutGroup +from .layout import TgoLayout, TgoLayoutGroup, TgoLayoutUnitGroup from .layoutloader import LayoutLoader LAYOUTS = LayoutLoader() diff --git a/game/layout/layout.py b/game/layout/layout.py index b7f75d9d..38c0fac0 100644 --- a/game/layout/layout.py +++ b/game/layout/layout.py @@ -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): diff --git a/game/layout/layoutloader.py b/game/layout/layoutloader.py index 86953900..98bbedf2 100644 --- a/game/layout/layoutloader.py +++ b/game/layout/layoutloader.py @@ -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() diff --git a/game/layout/layoutmapping.py b/game/layout/layoutmapping.py index df345d94..12da9b08 100644 --- a/game/layout/layoutmapping.py +++ b/game/layout/layoutmapping.py @@ -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 diff --git a/qt_ui/windows/groundobject/QGroundObjectBuyMenu.py b/qt_ui/windows/groundobject/QGroundObjectBuyMenu.py index b0286fe0..b0dc5505 100644 --- a/qt_ui/windows/groundobject/QGroundObjectBuyMenu.py +++ b/qt_ui/windows/groundobject/QGroundObjectBuyMenu.py @@ -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()