mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
- Fix tgogenerator - Fix UI for ForceGroup and Layouts - Fix ammo depot handling - Split bigger files in smaller meaningful files (TGO, layouts, forces) - Renamed Template to Layout - Renamed GroundGroup to TheaterGroup and GroundUnit to TheaterUnit - Reorganize Layouts and UnitGroups to a ArmedForces class and ForceGroup similar to the AirWing and Squadron - Reworded the UnitClass, GroupRole, GroupTask (adopted to PEP8) and reworked the connection from Role and Task - added comments - added missing unit classes - added temp workaround for missing classes - add repariable property to TheaterUnit - Review and Cleanup Added serialization for loaded templates Loading the templates from the .miz files takes a lot of computation time and in the future there will be more templates added to the system. Therefore a local pickle serialization for the loaded templates was re-added: - The pickle will be created the first time the TemplateLoader will be accessed - Pickle is stored in Liberation SaveDir - Added UI option to (re-)import templates
251 lines
9.5 KiB
Python
251 lines
9.5 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import random
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import ClassVar, TYPE_CHECKING, Type, Any, Iterator, Optional
|
|
|
|
import yaml
|
|
from dcs import Point
|
|
|
|
from game import db
|
|
from game.data.groups import GroupRole, GroupTask
|
|
from game.data.radar_db import UNITS_WITH_RADAR
|
|
from game.dcs.groundunittype import GroundUnitType
|
|
from game.dcs.shipunittype import ShipUnitType
|
|
from game.dcs.unittype import UnitType
|
|
from game.point_with_heading import PointWithHeading
|
|
from game.layout.layout import TheaterLayout, AntiAirLayout, GroupLayout
|
|
from dcs.unittype import UnitType as DcsUnitType, VehicleType, ShipType, StaticType
|
|
|
|
from game.theater.theatergroup import TheaterGroup
|
|
|
|
if TYPE_CHECKING:
|
|
from game import Game
|
|
from game.factions.faction import Faction
|
|
from game.theater import TheaterGroundObject, ControlPoint
|
|
|
|
|
|
@dataclass
|
|
class ForceGroup:
|
|
"""A logical group of multiple units and layouts which have a specific tasking"""
|
|
|
|
name: str
|
|
units: list[UnitType[Any]]
|
|
statics: list[Type[DcsUnitType]]
|
|
role: GroupRole
|
|
tasks: list[GroupTask] = field(default_factory=list)
|
|
layouts: list[TheaterLayout] = field(default_factory=list)
|
|
|
|
_by_name: ClassVar[dict[str, ForceGroup]] = {}
|
|
_by_role: ClassVar[dict[GroupRole, list[ForceGroup]]] = {}
|
|
_loaded: bool = False
|
|
|
|
@staticmethod
|
|
def for_layout(layout: TheaterLayout, faction: Faction) -> ForceGroup:
|
|
"""TODO Documentation"""
|
|
units: set[UnitType[Any]] = set()
|
|
statics: set[Type[DcsUnitType]] = set()
|
|
for group in layout.groups:
|
|
for unit_type in group.possible_types_for_faction(faction):
|
|
if issubclass(unit_type, VehicleType):
|
|
units.add(next(GroundUnitType.for_dcs_type(unit_type)))
|
|
elif issubclass(unit_type, ShipType):
|
|
units.add(next(ShipUnitType.for_dcs_type(unit_type)))
|
|
elif issubclass(unit_type, StaticType):
|
|
statics.add(unit_type)
|
|
|
|
return ForceGroup(
|
|
f"{layout.role.value}: {', '.join([t.description for t in layout.tasks])}",
|
|
list(units),
|
|
list(statics),
|
|
layout.role,
|
|
layout.tasks,
|
|
[layout],
|
|
)
|
|
|
|
def __str__(self) -> str:
|
|
return self.name
|
|
|
|
@classmethod
|
|
def named(cls, name: str) -> ForceGroup:
|
|
if not cls._loaded:
|
|
cls._load_all()
|
|
return cls._by_name[name]
|
|
|
|
def has_access_to_dcs_type(self, type: Type[DcsUnitType]) -> bool:
|
|
return (
|
|
any(unit.dcs_unit_type == type for unit in self.units)
|
|
or type in self.statics
|
|
)
|
|
|
|
def dcs_unit_types_for_group(self, group: GroupLayout) -> list[Type[DcsUnitType]]:
|
|
"""TODO Description"""
|
|
unit_types = [t for t in group.unit_types if self.has_access_to_dcs_type(t)]
|
|
|
|
alternative_types = []
|
|
for accessible_unit in self.units:
|
|
if accessible_unit.unit_class in group.unit_classes:
|
|
unit_types.append(accessible_unit.dcs_unit_type)
|
|
if accessible_unit.unit_class in group.alternative_classes:
|
|
alternative_types.append(accessible_unit.dcs_unit_type)
|
|
|
|
return unit_types or alternative_types
|
|
|
|
def unit_types_for_group(self, group: GroupLayout) -> Iterator[UnitType[Any]]:
|
|
for dcs_type in self.dcs_unit_types_for_group(group):
|
|
if issubclass(dcs_type, VehicleType):
|
|
yield next(GroundUnitType.for_dcs_type(dcs_type))
|
|
elif issubclass(dcs_type, ShipType):
|
|
yield next(ShipUnitType.for_dcs_type(dcs_type))
|
|
|
|
def statics_for_group(self, group: GroupLayout) -> Iterator[Type[DcsUnitType]]:
|
|
for dcs_type in self.dcs_unit_types_for_group(group):
|
|
if issubclass(dcs_type, StaticType):
|
|
yield dcs_type
|
|
|
|
def random_dcs_unit_type_for_group(self, group: GroupLayout) -> Type[DcsUnitType]:
|
|
"""TODO Description"""
|
|
return random.choice(self.dcs_unit_types_for_group(group))
|
|
|
|
def update_group(self, new_group: ForceGroup) -> None:
|
|
"""Update the group from another group. This will merge statics and layouts."""
|
|
# Merge layouts and statics
|
|
self.statics = list(set(self.statics + new_group.statics))
|
|
self.layouts = list(set(self.layouts + new_group.layouts))
|
|
|
|
def generate(
|
|
self,
|
|
name: str,
|
|
position: PointWithHeading,
|
|
control_point: ControlPoint,
|
|
game: Game,
|
|
) -> TheaterGroundObject:
|
|
"""Create a random TheaterGroundObject from the available templates"""
|
|
layout = random.choice(self.layouts)
|
|
return self.create_ground_object_for_layout(
|
|
layout, name, position, control_point, game
|
|
)
|
|
|
|
def create_ground_object_for_layout(
|
|
self,
|
|
layout: TheaterLayout,
|
|
name: str,
|
|
position: PointWithHeading,
|
|
control_point: ControlPoint,
|
|
game: Game,
|
|
) -> TheaterGroundObject:
|
|
"""Create a TheaterGroundObject for the given template"""
|
|
go = layout.create_ground_object(name, position, control_point)
|
|
# Generate all groups using the randomization if it defined
|
|
for group in layout.groups:
|
|
# Choose a random unit_type for the group
|
|
try:
|
|
unit_type = self.random_dcs_unit_type_for_group(group)
|
|
except IndexError:
|
|
if group.optional:
|
|
# If group is optional it is ok when no unit_type is available
|
|
continue
|
|
# if non-optional this is a error
|
|
raise RuntimeError(f"No accessible unit for {self.name} - {group.name}")
|
|
self.create_theater_group_for_tgo(go, group, name, game, unit_type)
|
|
|
|
return go
|
|
|
|
def create_theater_group_for_tgo(
|
|
self,
|
|
ground_object: TheaterGroundObject,
|
|
group: GroupLayout,
|
|
name: str,
|
|
game: Game,
|
|
unit_type: Type[DcsUnitType],
|
|
unit_count: Optional[int] = None,
|
|
) -> None:
|
|
"""Create a TheaterGroup and add it to the given TGO"""
|
|
# Random UnitCounter if not forced
|
|
if unit_count is None:
|
|
unit_count = group.unit_counter
|
|
# Static and non Static groups have to be separated
|
|
group_id = group.group - 1
|
|
if len(ground_object.groups) <= group_id:
|
|
# Requested group was not yet created
|
|
ground_group = TheaterGroup.from_template(
|
|
game.next_group_id(), group, ground_object, unit_type, unit_count
|
|
)
|
|
# Set Group Name
|
|
ground_group.name = f"{name} {group_id}"
|
|
ground_object.groups.append(ground_group)
|
|
units = ground_group.units
|
|
else:
|
|
ground_group = ground_object.groups[group_id]
|
|
units = group.generate_units(ground_object, unit_type, unit_count)
|
|
ground_group.units.extend(units)
|
|
|
|
# Assign UniqueID, name and align relative to ground_object
|
|
for u_id, unit in enumerate(units):
|
|
unit.id = game.next_unit_id()
|
|
unit.name = unit.unit_type.name if unit.unit_type else unit.type.name
|
|
unit.position = PointWithHeading.from_point(
|
|
Point(
|
|
ground_object.position.x + unit.position.x,
|
|
ground_object.position.y + unit.position.y,
|
|
),
|
|
# Align heading to GroundObject defined by the campaign designer
|
|
unit.position.heading + ground_object.heading,
|
|
)
|
|
if (
|
|
isinstance(self, AntiAirLayout)
|
|
and unit.unit_type
|
|
and unit.unit_type.dcs_unit_type in UNITS_WITH_RADAR
|
|
):
|
|
# Head Radars towards the center of the conflict
|
|
unit.position.heading = (
|
|
game.theater.heading_to_conflict_from(unit.position)
|
|
or unit.position.heading
|
|
)
|
|
# Rotate unit around the center to align the orientation of the group
|
|
unit.position.rotate(ground_object.position, ground_object.heading)
|
|
|
|
@classmethod
|
|
def _load_all(cls) -> None:
|
|
for file in Path("resources/units/groups").glob("*.yaml"):
|
|
if not file.is_file():
|
|
raise RuntimeError(f"{file.name} is not a valid ForceGroup")
|
|
|
|
with file.open(encoding="utf-8") as data_file:
|
|
data = yaml.safe_load(data_file)
|
|
|
|
group_role = GroupRole(data.get("role"))
|
|
|
|
group_tasks = [GroupTask.by_description(n) for n in data.get("tasks", [])]
|
|
|
|
units = [UnitType.named(unit) for unit in data.get("units", [])]
|
|
|
|
statics = []
|
|
for static in data.get("statics", []):
|
|
static_type = db.static_type_from_name(static)
|
|
if static_type is None:
|
|
logging.error(f"Static {static} for {file} is not valid")
|
|
else:
|
|
statics.append(static_type)
|
|
|
|
layouts = [next(db.LAYOUTS.by_name(n)) for n in data.get("layouts")]
|
|
|
|
force_group = ForceGroup(
|
|
name=data.get("name"),
|
|
units=units,
|
|
statics=statics,
|
|
role=group_role,
|
|
tasks=group_tasks,
|
|
layouts=layouts,
|
|
)
|
|
|
|
cls._by_name[force_group.name] = force_group
|
|
if group_role in cls._by_role:
|
|
cls._by_role[group_role].append(force_group)
|
|
else:
|
|
cls._by_role[group_role] = [force_group]
|
|
|
|
cls._loaded = True
|