mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Refactor Templates to Layouts, Review and Cleanup
- 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
This commit is contained in:
82
game/armedforces/armedforces.py
Normal file
82
game/armedforces/armedforces.py
Normal file
@@ -0,0 +1,82 @@
|
||||
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.armedforces.forcegroup import ForceGroup
|
||||
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]]
|
||||
|
||||
def __init__(self, faction: Faction):
|
||||
with logged_duration(f"Loading armed forces for {faction.name}"):
|
||||
self._load_forces(faction)
|
||||
|
||||
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
|
||||
# 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]
|
||||
|
||||
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 = {}
|
||||
|
||||
preset_layouts = [
|
||||
layout
|
||||
for preset_group in faction.preset_groups
|
||||
for layout in preset_group.layouts
|
||||
]
|
||||
|
||||
# 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):
|
||||
# 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
|
||||
|
||||
def groups_for_tasks(self, tasks: list[GroupTask]) -> list[ForceGroup]:
|
||||
groups = []
|
||||
for task in tasks:
|
||||
for group in self.groups_for_task(task):
|
||||
if group not in groups:
|
||||
groups.append(group)
|
||||
return groups
|
||||
|
||||
def random_group_for_task(self, group_task: GroupTask) -> Optional[ForceGroup]:
|
||||
unit_groups = list(self.groups_for_task(group_task))
|
||||
return random.choice(unit_groups) if unit_groups else None
|
||||
250
game/armedforces/forcegroup.py
Normal file
250
game/armedforces/forcegroup.py
Normal file
@@ -0,0 +1,250 @@
|
||||
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
|
||||
@@ -9,6 +9,7 @@ from game.campaignloader import CampaignAirWingConfig
|
||||
from game.campaignloader.defaultsquadronassigner import DefaultSquadronAssigner
|
||||
from game.commander import TheaterCommander
|
||||
from game.commander.missionscheduler import MissionScheduler
|
||||
from game.armedforces.armedforces import ArmedForces
|
||||
from game.income import Income
|
||||
from game.navmesh import NavMesh
|
||||
from game.orderedset import OrderedSet
|
||||
@@ -41,6 +42,7 @@ class Coalition:
|
||||
self.bullseye = Bullseye(Point(0, 0))
|
||||
self.faker = Faker(self.faction.locales)
|
||||
self.air_wing = AirWing(player, game, self.faction)
|
||||
self.armed_forces = ArmedForces(self.faction)
|
||||
self.transfers = PendingTransfers(game, player)
|
||||
|
||||
# Late initialized because the two coalitions in the game are mutually
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from game.theater.theatergroup import TheaterUnit
|
||||
|
||||
|
||||
class AlicCodes:
|
||||
CODES = {
|
||||
@@ -37,5 +39,5 @@ class AlicCodes:
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def code_for(cls, unit_type: str) -> int:
|
||||
return cls.CODES[unit_type]
|
||||
def code_for(cls, unit: TheaterUnit) -> int:
|
||||
return cls.CODES[unit.type.id]
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import inspect
|
||||
import dcs
|
||||
|
||||
REQUIRED_BUILDINGS = [
|
||||
"ammo",
|
||||
"factory",
|
||||
"fob",
|
||||
]
|
||||
|
||||
DEFAULT_AVAILABLE_BUILDINGS = [
|
||||
"fuel",
|
||||
"comms",
|
||||
|
||||
@@ -104,13 +104,13 @@ MODERN_DOCTRINE = Doctrine(
|
||||
sweep_distance=nautical_miles(60),
|
||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||
{
|
||||
UnitClass.Tank: 3,
|
||||
UnitClass.Atgm: 2,
|
||||
UnitClass.Apc: 2,
|
||||
UnitClass.Ifv: 3,
|
||||
UnitClass.Artillery: 1,
|
||||
UnitClass.TANK: 3,
|
||||
UnitClass.ATGM: 2,
|
||||
UnitClass.APC: 2,
|
||||
UnitClass.IFV: 3,
|
||||
UnitClass.ARTILLERY: 1,
|
||||
UnitClass.SHORAD: 2,
|
||||
UnitClass.Recon: 1,
|
||||
UnitClass.RECON: 1,
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -141,13 +141,13 @@ COLDWAR_DOCTRINE = Doctrine(
|
||||
sweep_distance=nautical_miles(40),
|
||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||
{
|
||||
UnitClass.Tank: 4,
|
||||
UnitClass.Atgm: 2,
|
||||
UnitClass.Apc: 3,
|
||||
UnitClass.Ifv: 2,
|
||||
UnitClass.Artillery: 1,
|
||||
UnitClass.TANK: 4,
|
||||
UnitClass.ATGM: 2,
|
||||
UnitClass.APC: 3,
|
||||
UnitClass.IFV: 2,
|
||||
UnitClass.ARTILLERY: 1,
|
||||
UnitClass.SHORAD: 2,
|
||||
UnitClass.Recon: 1,
|
||||
UnitClass.RECON: 1,
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -178,12 +178,12 @@ WWII_DOCTRINE = Doctrine(
|
||||
sweep_distance=nautical_miles(10),
|
||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||
{
|
||||
UnitClass.Tank: 3,
|
||||
UnitClass.Atgm: 3,
|
||||
UnitClass.Apc: 3,
|
||||
UnitClass.Artillery: 1,
|
||||
UnitClass.TANK: 3,
|
||||
UnitClass.ATGM: 3,
|
||||
UnitClass.APC: 3,
|
||||
UnitClass.ARTILLERY: 1,
|
||||
UnitClass.SHORAD: 3,
|
||||
UnitClass.Recon: 1,
|
||||
UnitClass.RECON: 1,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,63 +1,69 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class GroupRole(Enum):
|
||||
Unknow = "Unknown"
|
||||
AntiAir = "AntiAir"
|
||||
Building = "Building"
|
||||
Naval = "Naval"
|
||||
GroundForce = "GroundForce"
|
||||
Defenses = "Defenses"
|
||||
Air = "Air"
|
||||
"""Role of a ForceGroup within the ArmedForces"""
|
||||
|
||||
AIR_DEFENSE = "AntiAir"
|
||||
BUILDING = "Building"
|
||||
DEFENSES = "Defenses"
|
||||
GROUND_FORCE = "GroundForce"
|
||||
NAVAL = "Naval"
|
||||
|
||||
@property
|
||||
def tasks(self) -> list[GroupTask]:
|
||||
return [task for task in GroupTask if task.role == self]
|
||||
|
||||
|
||||
class GroupTask(Enum):
|
||||
EWR = "EarlyWarningRadar"
|
||||
AAA = "AAA"
|
||||
SHORAD = "SHORAD"
|
||||
MERAD = "MERAD"
|
||||
LORAD = "LORAD"
|
||||
AircraftCarrier = "AircraftCarrier"
|
||||
HelicopterCarrier = "HelicopterCarrier"
|
||||
Navy = "Navy"
|
||||
BaseDefense = "BaseDefense" # Ground
|
||||
FrontLine = "FrontLine"
|
||||
Air = "Air"
|
||||
Missile = "Missile"
|
||||
Coastal = "Coastal"
|
||||
Factory = "Factory"
|
||||
Ammo = "Ammo"
|
||||
Oil = "Oil"
|
||||
FOB = "FOB"
|
||||
StrikeTarget = "StrikeTarget"
|
||||
Comms = "Comms"
|
||||
Power = "Power"
|
||||
"""Specific Tasking of a ForceGroup"""
|
||||
|
||||
def __init__(self, description: str, role: GroupRole):
|
||||
self.description = description
|
||||
self.role = role
|
||||
|
||||
ROLE_TASKINGS: dict[GroupRole, list[GroupTask]] = {
|
||||
GroupRole.Unknow: [], # No Tasking
|
||||
GroupRole.AntiAir: [
|
||||
GroupTask.EWR,
|
||||
GroupTask.AAA,
|
||||
GroupTask.SHORAD,
|
||||
GroupTask.MERAD,
|
||||
GroupTask.LORAD,
|
||||
],
|
||||
GroupRole.GroundForce: [GroupTask.BaseDefense, GroupTask.FrontLine],
|
||||
GroupRole.Naval: [
|
||||
GroupTask.AircraftCarrier,
|
||||
GroupTask.HelicopterCarrier,
|
||||
GroupTask.Navy,
|
||||
],
|
||||
GroupRole.Building: [
|
||||
GroupTask.Factory,
|
||||
GroupTask.Ammo,
|
||||
GroupTask.Oil,
|
||||
GroupTask.FOB,
|
||||
GroupTask.StrikeTarget,
|
||||
GroupTask.Comms,
|
||||
GroupTask.Power,
|
||||
],
|
||||
GroupRole.Defenses: [GroupTask.Missile, GroupTask.Coastal],
|
||||
GroupRole.Air: [GroupTask.Air],
|
||||
}
|
||||
@classmethod
|
||||
def by_description(cls, description: str) -> GroupTask:
|
||||
for task in GroupTask:
|
||||
if task.description == description:
|
||||
return task
|
||||
raise RuntimeError(f"GroupTask with description {description} does not exist")
|
||||
|
||||
# ANTI AIR
|
||||
AAA = ("AAA", GroupRole.AIR_DEFENSE)
|
||||
EARLY_WARNING_RADAR = ("EarlyWarningRadar", GroupRole.AIR_DEFENSE)
|
||||
LORAD = ("LORAD", GroupRole.AIR_DEFENSE)
|
||||
MERAD = ("MERAD", GroupRole.AIR_DEFENSE)
|
||||
SHORAD = ("SHORAD", GroupRole.AIR_DEFENSE)
|
||||
|
||||
# NAVAL
|
||||
AIRCRAFT_CARRIER = ("AircraftCarrier", GroupRole.NAVAL)
|
||||
HELICOPTER_CARRIER = ("HelicopterCarrier", GroupRole.NAVAL)
|
||||
NAVY = ("Navy", GroupRole.NAVAL)
|
||||
|
||||
# GROUND FORCES
|
||||
BASE_DEFENSE = ("BaseDefense", GroupRole.GROUND_FORCE)
|
||||
FRONT_LINE = ("FrontLine", GroupRole.GROUND_FORCE)
|
||||
|
||||
# DEFENSES
|
||||
COASTAL = ("Coastal", GroupRole.DEFENSES)
|
||||
MISSILE = ("Missile", GroupRole.DEFENSES)
|
||||
|
||||
# BUILDINGS
|
||||
ALLY_CAMP = ("AllyCamp", GroupRole.BUILDING)
|
||||
AMMO = ("Ammo", GroupRole.BUILDING)
|
||||
COMMS = ("Comms", GroupRole.BUILDING)
|
||||
DERRICK = ("Derrick", GroupRole.BUILDING)
|
||||
FACTORY = ("Factory", GroupRole.BUILDING)
|
||||
FARP = ("Farp", GroupRole.BUILDING)
|
||||
FOB = ("FOB", GroupRole.BUILDING)
|
||||
FUEL = ("Fuel", GroupRole.BUILDING)
|
||||
OFFSHORE_STRIKE_TARGET = ("OffShoreStrikeTarget", GroupRole.BUILDING)
|
||||
OIL = ("Oil", GroupRole.BUILDING)
|
||||
POWER = ("Power", GroupRole.BUILDING)
|
||||
STRIKE_TARGET = ("StrikeTarget", GroupRole.BUILDING)
|
||||
VILLAGE = ("Village", GroupRole.BUILDING)
|
||||
WARE = ("Ware", GroupRole.BUILDING)
|
||||
WW2_BUNKER = ("WW2Bunker", GroupRole.BUILDING)
|
||||
|
||||
@@ -2,39 +2,40 @@ from __future__ import annotations
|
||||
|
||||
from enum import unique, Enum
|
||||
|
||||
from game.data.groups import GroupRole, GroupTask
|
||||
|
||||
|
||||
@unique
|
||||
class UnitClass(Enum):
|
||||
Unknown = "Unknown"
|
||||
Tank = "Tank"
|
||||
Atgm = "ATGM"
|
||||
Ifv = "IFV"
|
||||
Apc = "APC"
|
||||
Artillery = "Artillery"
|
||||
Logistics = "Logistics"
|
||||
Recon = "Recon"
|
||||
Infantry = "Infantry"
|
||||
UNKNOWN = "Unknown"
|
||||
AAA = "AAA"
|
||||
AIRCRAFT_CARRIER = "AircraftCarrier"
|
||||
APC = "APC"
|
||||
ARTILLERY = "Artillery"
|
||||
ATGM = "ATGM"
|
||||
BOAT = "Boat"
|
||||
COMMAND_POST = "CommandPost"
|
||||
CRUISER = "Cruiser"
|
||||
DESTROYER = "Destroyer"
|
||||
EARLY_WARNING_RADAR = "EarlyWarningRadar"
|
||||
FORTIFICATION = "Fortification"
|
||||
FRIGATE = "Frigate"
|
||||
HELICOPTER_CARRIER = "HelicopterCarrier"
|
||||
IFV = "IFV"
|
||||
INFANTRY = "Infantry"
|
||||
LANDING_SHIP = "LandingShip"
|
||||
LAUNCHER = "Launcher"
|
||||
LOGISTICS = "Logistics"
|
||||
MANPAD = "Manpad"
|
||||
MISSILE = "Missile"
|
||||
OPTICAL_TRACKER = "OpticalTracker"
|
||||
PLANE = "Plane"
|
||||
POWER = "Power"
|
||||
RECON = "Recon"
|
||||
SEARCH_LIGHT = "SearchLight"
|
||||
SEARCH_RADAR = "SearchRadar"
|
||||
SEARCH_TRACK_RADAR = "SearchTrackRadar"
|
||||
SHORAD = "SHORAD"
|
||||
Manpad = "Manpad"
|
||||
SR = "SearchRadar"
|
||||
STR = "SearchTrackRadar"
|
||||
LowAltSR = "LowAltSearchRadar"
|
||||
TR = "TrackRadar"
|
||||
LN = "Launcher"
|
||||
EWR = "EarlyWarningRadar"
|
||||
SPECIALIZED_RADAR = "SpecializedRadar"
|
||||
SUBMARINE = "Submarine"
|
||||
TANK = "Tank"
|
||||
TELAR = "TELAR"
|
||||
Missile = "Missile"
|
||||
AircraftCarrier = "AircraftCarrier"
|
||||
HelicopterCarrier = "HelicopterCarrier"
|
||||
Destroyer = "Destroyer"
|
||||
Cruiser = "Cruiser"
|
||||
Submarine = "Submarine"
|
||||
LandingShip = "LandingShip"
|
||||
Boat = "Boat"
|
||||
Plane = "Plane"
|
||||
|
||||
def to_dict(self) -> str:
|
||||
return self.value
|
||||
TRACK_RADAR = "TrackRadar"
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from typing import Any, ClassVar, Iterator, Optional, TYPE_CHECKING, Type
|
||||
from typing import Any, Iterator, Optional, TYPE_CHECKING, Type
|
||||
|
||||
import yaml
|
||||
from dcs.helicopters import helicopter_map
|
||||
@@ -397,5 +396,5 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
||||
channel_namer=radio_config.channel_namer,
|
||||
kneeboard_units=units,
|
||||
utc_kneeboard=data.get("utc_kneeboard", False),
|
||||
unit_class=UnitClass.Plane,
|
||||
unit_class=UnitClass.PLANE,
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Type, Optional, Iterator
|
||||
from typing import Type, Iterator
|
||||
|
||||
import yaml
|
||||
from dcs.unittype import VehicleType
|
||||
@@ -55,8 +55,11 @@ class GroundUnitType(UnitType[Type[VehicleType]]):
|
||||
introduction = "No data."
|
||||
|
||||
class_name = data.get("class")
|
||||
# TODO Exception handling for missing classes
|
||||
unit_class = UnitClass(class_name) if class_name else UnitClass.Unknown
|
||||
if class_name is None:
|
||||
logging.warning(f"{vehicle.id} has no class")
|
||||
unit_class = UnitClass.UNKNOWN
|
||||
else:
|
||||
unit_class = UnitClass(class_name)
|
||||
|
||||
for variant in data.get("variants", [vehicle.id]):
|
||||
yield GroundUnitType(
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Type, Optional, ClassVar, Iterator
|
||||
from typing import Type, Iterator
|
||||
|
||||
import yaml
|
||||
from dcs.ships import ship_map
|
||||
from dcs.unittype import VehicleType, ShipType
|
||||
from dcs.vehicles import vehicle_map
|
||||
from dcs.unittype import ShipType
|
||||
|
||||
from game.data.units import UnitClass
|
||||
from game.dcs.unittype import UnitType
|
||||
@@ -70,5 +68,5 @@ class ShipUnitType(UnitType[Type[ShipType]]):
|
||||
country_of_origin=data.get("origin", "No data."),
|
||||
manufacturer=data.get("manufacturer", "No data."),
|
||||
role=data.get("role", "No data."),
|
||||
price=data.get("price", 1),
|
||||
price=data.get("price"),
|
||||
)
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import itertools
|
||||
import random
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import ClassVar, TYPE_CHECKING, Any, Iterator
|
||||
|
||||
import yaml
|
||||
|
||||
from game.data.groups import GroupRole, GroupTask
|
||||
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 gen.templates import GroundObjectTemplate
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import TheaterGroundObject, ControlPoint
|
||||
|
||||
|
||||
@dataclass
|
||||
class UnitGroup:
|
||||
name: str
|
||||
ground_units: list[GroundUnitType]
|
||||
ship_units: list[ShipUnitType]
|
||||
statics: list[str]
|
||||
role: GroupRole
|
||||
tasks: list[GroupTask] = field(default_factory=list)
|
||||
template_names: list[str] = field(default_factory=list)
|
||||
|
||||
_by_name: ClassVar[dict[str, UnitGroup]] = {}
|
||||
_by_role: ClassVar[dict[GroupRole, list[UnitGroup]]] = {}
|
||||
_loaded: bool = False
|
||||
_templates: list[GroundObjectTemplate] = field(default_factory=list)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def update_from_unit_group(self, unit_group: UnitGroup) -> None:
|
||||
# Update tasking and templates
|
||||
self.tasks.extend([task for task in unit_group.tasks if task not in self.tasks])
|
||||
self._templates.extend(
|
||||
[
|
||||
template
|
||||
for template in unit_group.templates
|
||||
if template not in self.templates
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
def templates(self) -> list[GroundObjectTemplate]:
|
||||
return self._templates
|
||||
|
||||
def add_template(self, faction_template: GroundObjectTemplate) -> None:
|
||||
template = copy.deepcopy(faction_template)
|
||||
updated_groups = []
|
||||
for group in template.groups:
|
||||
unit_types = list(
|
||||
itertools.chain(
|
||||
[u.dcs_id for u in self.ground_units if group.can_use_unit(u)],
|
||||
[s.dcs_id for s in self.ship_units if group.can_use_unit(s)],
|
||||
[s for s in self.statics if group.can_use_unit_type(s)],
|
||||
)
|
||||
)
|
||||
if unit_types:
|
||||
group.set_possible_types(unit_types)
|
||||
updated_groups.append(group)
|
||||
template.groups = updated_groups
|
||||
self._templates.append(template)
|
||||
|
||||
def load_templates(self, faction: Faction) -> None:
|
||||
self._templates = []
|
||||
if self.template_names:
|
||||
# Preferred templates
|
||||
for template_name in self.template_names:
|
||||
template = faction.templates.by_name(template_name)
|
||||
if template:
|
||||
self.add_template(template)
|
||||
|
||||
if not self._templates:
|
||||
# Find all matching templates if no preferred set or available
|
||||
for template in list(
|
||||
faction.templates.for_role_and_tasks(self.role, self.tasks)
|
||||
):
|
||||
if any(self.has_unit_type(unit) for unit in template.units):
|
||||
self.add_template(template)
|
||||
|
||||
def set_templates(self, templates: list[GroundObjectTemplate]) -> None:
|
||||
self._templates = templates
|
||||
|
||||
def has_unit_type(self, unit_type: UnitType[Any]) -> bool:
|
||||
return unit_type in self.ground_units or unit_type in self.ship_units
|
||||
|
||||
@property
|
||||
def unit_types(self) -> Iterator[str]:
|
||||
for unit in self.ground_units:
|
||||
yield unit.dcs_id
|
||||
for ship in self.ship_units:
|
||||
yield ship.dcs_id
|
||||
for static in self.statics:
|
||||
yield static
|
||||
|
||||
@classmethod
|
||||
def named(cls, name: str) -> UnitGroup:
|
||||
if not cls._loaded:
|
||||
cls._load_all()
|
||||
return cls._by_name[name]
|
||||
|
||||
def generate(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
control_point: ControlPoint,
|
||||
game: Game,
|
||||
) -> TheaterGroundObject:
|
||||
template = random.choice(self.templates)
|
||||
return template.generate(name, position, control_point, game)
|
||||
|
||||
@classmethod
|
||||
def _load_all(cls) -> None:
|
||||
for file in Path("resources/units/unit_groups").glob("*.yaml"):
|
||||
if not file.is_file():
|
||||
continue
|
||||
|
||||
with file.open(encoding="utf-8") as data_file:
|
||||
data = yaml.safe_load(data_file)
|
||||
|
||||
group_role = GroupRole(data.get("role"))
|
||||
|
||||
group_tasks = [GroupTask(n) for n in data.get("tasks", [])]
|
||||
|
||||
ground_units = [
|
||||
GroundUnitType.named(n) for n in data.get("ground_units", [])
|
||||
]
|
||||
ship_units = [ShipUnitType.named(n) for n in data.get("ship_units", [])]
|
||||
|
||||
unit_group = UnitGroup(
|
||||
name=data.get("name"),
|
||||
ground_units=ground_units,
|
||||
ship_units=ship_units,
|
||||
statics=data.get("statics", []),
|
||||
role=group_role,
|
||||
tasks=group_tasks,
|
||||
template_names=data.get("templates", []),
|
||||
)
|
||||
|
||||
cls._by_name[unit_group.name] = unit_group
|
||||
if group_role in cls._by_role:
|
||||
cls._by_role[group_role].append(unit_group)
|
||||
else:
|
||||
cls._by_role[group_role] = [unit_group]
|
||||
|
||||
cls._loaded = True
|
||||
@@ -4,7 +4,7 @@ from abc import ABC
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from typing import TypeVar, Generic, Type, ClassVar, Any, Iterator, Optional
|
||||
from typing import TypeVar, Generic, Type, ClassVar, Any, Iterator
|
||||
|
||||
from dcs.unittype import UnitType as DcsUnitType
|
||||
|
||||
@@ -26,7 +26,9 @@ class UnitType(ABC, Generic[DcsUnitTypeT]):
|
||||
unit_class: UnitClass
|
||||
|
||||
_by_name: ClassVar[dict[str, UnitType[Any]]] = {}
|
||||
_by_unit_type: ClassVar[dict[DcsUnitTypeT, list[UnitType[Any]]]] = defaultdict(list)
|
||||
_by_unit_type: ClassVar[dict[Type[DcsUnitType], list[UnitType[Any]]]] = defaultdict(
|
||||
list
|
||||
)
|
||||
_loaded: ClassVar[bool] = False
|
||||
|
||||
def __str__(self) -> str:
|
||||
@@ -43,7 +45,7 @@ class UnitType(ABC, Generic[DcsUnitTypeT]):
|
||||
|
||||
@classmethod
|
||||
def named(cls, name: str) -> UnitType[Any]:
|
||||
raise NotImplementedError
|
||||
return cls._by_name[name]
|
||||
|
||||
@classmethod
|
||||
def for_dcs_type(cls, dcs_unit_type: DcsUnitTypeT) -> Iterator[UnitType[Any]]:
|
||||
|
||||
@@ -26,7 +26,7 @@ if TYPE_CHECKING:
|
||||
ConvoyUnit,
|
||||
FlyingUnit,
|
||||
FrontLineUnit,
|
||||
GroundObjectMapping,
|
||||
TheaterUnitMapping,
|
||||
UnitMap,
|
||||
SceneryObjectMapping,
|
||||
)
|
||||
@@ -72,8 +72,8 @@ class GroundLosses:
|
||||
player_airlifts: List[AirliftUnits] = field(default_factory=list)
|
||||
enemy_airlifts: List[AirliftUnits] = field(default_factory=list)
|
||||
|
||||
player_ground_objects: List[GroundObjectMapping] = field(default_factory=list)
|
||||
enemy_ground_objects: List[GroundObjectMapping] = field(default_factory=list)
|
||||
player_ground_objects: List[TheaterUnitMapping] = field(default_factory=list)
|
||||
enemy_ground_objects: List[TheaterUnitMapping] = field(default_factory=list)
|
||||
|
||||
player_scenery: List[SceneryObjectMapping] = field(default_factory=list)
|
||||
enemy_scenery: List[SceneryObjectMapping] = field(default_factory=list)
|
||||
@@ -158,7 +158,7 @@ class Debriefing:
|
||||
yield from self.ground_losses.enemy_airlifts
|
||||
|
||||
@property
|
||||
def ground_object_losses(self) -> Iterator[GroundObjectMapping]:
|
||||
def ground_object_losses(self) -> Iterator[TheaterUnitMapping]:
|
||||
yield from self.ground_losses.player_ground_objects
|
||||
yield from self.ground_losses.enemy_ground_objects
|
||||
|
||||
@@ -224,15 +224,7 @@ class Debriefing:
|
||||
else:
|
||||
losses = self.ground_losses.enemy_ground_objects
|
||||
for loss in losses:
|
||||
# We do not have handling for ships and statics UniType yet so we have to
|
||||
# take more care here. Fallback for ship and static is to use the type str
|
||||
# which is the dcs_type.id
|
||||
unit_type = (
|
||||
loss.ground_unit.unit_type.name
|
||||
if loss.ground_unit.unit_type
|
||||
else loss.ground_unit.type
|
||||
)
|
||||
losses_by_type[unit_type] += 1
|
||||
losses_by_type[loss.theater_unit.type.id] += 1
|
||||
return losses_by_type
|
||||
|
||||
def scenery_losses_by_type(self, player: bool) -> Dict[str, int]:
|
||||
@@ -286,9 +278,9 @@ class Debriefing:
|
||||
losses.enemy_cargo_ships.append(cargo_ship)
|
||||
continue
|
||||
|
||||
ground_object = self.unit_map.ground_object(unit_name)
|
||||
ground_object = self.unit_map.theater_units(unit_name)
|
||||
if ground_object is not None:
|
||||
if ground_object.ground_unit.ground_object.is_friendly(to_player=True):
|
||||
if ground_object.theater_unit.ground_object.is_friendly(to_player=True):
|
||||
losses.player_ground_objects.append(ground_object)
|
||||
else:
|
||||
losses.enemy_ground_objects.append(ground_object)
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass, field
|
||||
from functools import cached_property
|
||||
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING
|
||||
|
||||
import dcs
|
||||
from dcs.countries import country_dict
|
||||
from dcs.unittype import ShipType
|
||||
from dcs.unittype import ShipType, StaticType
|
||||
from dcs.unittype import UnitType as DcsUnitType
|
||||
|
||||
from game.data.building_data import (
|
||||
WW2_ALLIES_BUILDINGS,
|
||||
DEFAULT_AVAILABLE_BUILDINGS,
|
||||
WW2_GERMANY_BUILDINGS,
|
||||
WW2_FREE,
|
||||
REQUIRED_BUILDINGS,
|
||||
)
|
||||
from game.data.doctrine import (
|
||||
Doctrine,
|
||||
@@ -24,18 +25,12 @@ from game.data.doctrine import (
|
||||
WWII_DOCTRINE,
|
||||
)
|
||||
from game.data.units import UnitClass
|
||||
from game.data.groups import GroupRole, GroupTask
|
||||
from game import db
|
||||
from game.data.groups import GroupRole
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.dcs.shipunittype import ShipUnitType
|
||||
from game.dcs.unitgroup import UnitGroup
|
||||
from game.armedforces.forcegroup import ForceGroup
|
||||
from game.dcs.unittype import UnitType
|
||||
from gen.templates import (
|
||||
GroundObjectTemplates,
|
||||
GroundObjectTemplate,
|
||||
GroupTemplate,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater.start_generator import ModSettings
|
||||
@@ -84,7 +79,7 @@ class Faction:
|
||||
air_defense_units: List[GroundUnitType] = field(default_factory=list)
|
||||
|
||||
# A list of all supported sets of units
|
||||
preset_groups: list[UnitGroup] = field(default_factory=list)
|
||||
preset_groups: list[ForceGroup] = field(default_factory=list)
|
||||
|
||||
# Possible Missile site generators for this faction
|
||||
missiles: List[GroundUnitType] = field(default_factory=list)
|
||||
@@ -110,7 +105,7 @@ class Faction:
|
||||
# doctrine
|
||||
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
|
||||
|
||||
# List of available building templates for this faction
|
||||
# List of available building layouts for this faction
|
||||
building_set: List[str] = field(default_factory=list)
|
||||
|
||||
# List of default livery overrides
|
||||
@@ -125,47 +120,24 @@ class Faction:
|
||||
#: both will use it.
|
||||
unrestricted_satnav: bool = False
|
||||
|
||||
# All possible templates which can be generated by the faction
|
||||
templates: GroundObjectTemplates = field(default=GroundObjectTemplates())
|
||||
|
||||
# All available unit_groups
|
||||
unit_groups: dict[GroupRole, list[UnitGroup]] = field(default_factory=dict)
|
||||
|
||||
# Save all accessible units for performance increase
|
||||
_accessible_units: list[UnitType[Any]] = field(default_factory=list)
|
||||
|
||||
def __getitem__(self, item: str) -> Any:
|
||||
return getattr(self, item)
|
||||
|
||||
@property
|
||||
def accessible_units(self) -> Iterator[UnitType[Any]]:
|
||||
yield from self._accessible_units
|
||||
|
||||
@property
|
||||
def air_defenses(self) -> list[str]:
|
||||
"""Returns the Air Defense types"""
|
||||
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.AntiAir]
|
||||
)
|
||||
return sorted(air_defenses)
|
||||
|
||||
def has_access_to_unit_type(self, unit_type: str) -> bool:
|
||||
# GroundUnits
|
||||
if any(unit_type == u.dcs_id for u in self.accessible_units):
|
||||
def has_access_to_dcs_type(self, unit_type: Type[DcsUnitType]) -> bool:
|
||||
# Vehicle and Ship Units
|
||||
if any(unit_type == u.dcs_unit_type for u in self.accessible_units):
|
||||
return True
|
||||
|
||||
# Statics
|
||||
if db.static_type_from_name(unit_type) is not None:
|
||||
if issubclass(unit_type, StaticType):
|
||||
# TODO Improve the statics checking
|
||||
# We currently do not have any list or similar to check if a faction has
|
||||
# access to a specific static. There we accept any static here
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_access_to_unit_class(self, unit_class: UnitClass) -> bool:
|
||||
return any(unit.unit_class is unit_class for unit in self.accessible_units)
|
||||
|
||||
def _load_accessible_units(self, templates: GroundObjectTemplates) -> None:
|
||||
self._accessible_units = []
|
||||
@cached_property
|
||||
def accessible_units(self) -> list[UnitType[Any]]:
|
||||
all_units: Iterator[UnitType[Any]] = itertools.chain(
|
||||
self.ground_units,
|
||||
self.infantry_units,
|
||||
@@ -173,138 +145,22 @@ class Faction:
|
||||
self.naval_units,
|
||||
self.missiles,
|
||||
(
|
||||
ground_unit
|
||||
unit
|
||||
for preset_group in self.preset_groups
|
||||
for ground_unit in preset_group.ground_units
|
||||
),
|
||||
(
|
||||
ship_unit
|
||||
for preset_group in self.preset_groups
|
||||
for ship_unit in preset_group.ship_units
|
||||
for unit in preset_group.units
|
||||
),
|
||||
)
|
||||
for unit in all_units:
|
||||
if unit not in self._accessible_units:
|
||||
self._accessible_units.append(unit)
|
||||
return list(all_units)
|
||||
|
||||
def initialize(
|
||||
self, all_templates: GroundObjectTemplates, mod_settings: ModSettings
|
||||
) -> None:
|
||||
# Apply the mod settings
|
||||
self._apply_mod_settings(mod_settings)
|
||||
# Load all accessible units and store them for performant later usage
|
||||
self._load_accessible_units(all_templates)
|
||||
# Load all faction compatible templates
|
||||
self._load_templates(all_templates)
|
||||
# Load Unit Groups
|
||||
self._load_unit_groups()
|
||||
|
||||
def _add_unit_group(self, unit_group: UnitGroup, merge: bool = True) -> None:
|
||||
if not unit_group.templates:
|
||||
unit_group.load_templates(self)
|
||||
if not unit_group.templates:
|
||||
# Empty templates will throw an error on generation
|
||||
logging.error(
|
||||
f"Skipping Unit group {unit_group.name} as no templates are available to generate the group"
|
||||
)
|
||||
return
|
||||
if unit_group.role in self.unit_groups:
|
||||
for group in self.unit_groups[unit_group.role]:
|
||||
if merge and all(task in group.tasks for task in unit_group.tasks):
|
||||
# Update existing group if same tasking
|
||||
group.update_from_unit_group(unit_group)
|
||||
return
|
||||
# Add new Unit_group
|
||||
self.unit_groups[unit_group.role].append(unit_group)
|
||||
else:
|
||||
self.unit_groups[unit_group.role] = [unit_group]
|
||||
|
||||
def _load_unit_groups(self) -> None:
|
||||
# This function will create all the UnitGroups for the faction
|
||||
# It will create a unit group for each global Template, Building or
|
||||
# Legacy supported templates (not yet migrated from the generators).
|
||||
# For every preset_group there will be a separate UnitGroup so no mixed
|
||||
# UnitGroups will be generated for them. Special groups like complex SAM Systems
|
||||
self.unit_groups = {}
|
||||
|
||||
# Generate UnitGroups for all global templates
|
||||
for role, template in self.templates.templates:
|
||||
if template.generic or role == GroupRole.Building:
|
||||
# Build groups for global templates and buildings
|
||||
self._add_group_for_template(role, template)
|
||||
|
||||
# Add preset groups
|
||||
for preset_group in self.preset_groups:
|
||||
# Add as separate group, do not merge with generic groups!
|
||||
self._add_unit_group(preset_group, False)
|
||||
|
||||
def _add_group_for_template(
|
||||
self, role: GroupRole, template: GroundObjectTemplate
|
||||
) -> None:
|
||||
unit_group = UnitGroup(
|
||||
f"{role.value}: {', '.join([t.value for t in template.tasks])}",
|
||||
[u for u in template.units if isinstance(u, GroundUnitType)],
|
||||
[u for u in template.units if isinstance(u, ShipUnitType)],
|
||||
list(template.statics),
|
||||
role,
|
||||
@property
|
||||
def air_defenses(self) -> list[str]:
|
||||
"""Returns the Air Defense types"""
|
||||
# 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]
|
||||
)
|
||||
unit_group.tasks = template.tasks
|
||||
unit_group.set_templates([template])
|
||||
self._add_unit_group(unit_group)
|
||||
|
||||
def initialize_group_template(
|
||||
self, group: GroupTemplate, faction_sensitive: bool = True
|
||||
) -> bool:
|
||||
# Sensitive defines if the initialization should check if the unit is available
|
||||
# to this faction or not. It is disabled for migration only atm.
|
||||
unit_types = [
|
||||
t
|
||||
for t in group.unit_types
|
||||
if not faction_sensitive or self.has_access_to_unit_type(t)
|
||||
]
|
||||
|
||||
alternative_types = []
|
||||
for accessible_unit in self.accessible_units:
|
||||
if accessible_unit.unit_class in group.unit_classes:
|
||||
unit_types.append(accessible_unit.dcs_id)
|
||||
if accessible_unit.unit_class in group.alternative_classes:
|
||||
alternative_types.append(accessible_unit.dcs_id)
|
||||
|
||||
if not unit_types and not alternative_types and not group.optional:
|
||||
raise StopIteration
|
||||
|
||||
types = unit_types or alternative_types
|
||||
group.set_possible_types(types)
|
||||
return len(types) > 0
|
||||
|
||||
def _load_templates(self, all_templates: GroundObjectTemplates) -> None:
|
||||
self.templates = GroundObjectTemplates()
|
||||
# This loads all templates which are usable by the faction
|
||||
for role, template in all_templates.templates:
|
||||
# Make a deep copy of a template and add it to the template_list.
|
||||
# This is required to have faction independent templates. Otherwise
|
||||
# the reference would be the same and changes would affect all.
|
||||
faction_template = copy.deepcopy(template)
|
||||
try:
|
||||
faction_template.groups[:] = [
|
||||
group_template
|
||||
for group_template in faction_template.groups
|
||||
if self.initialize_group_template(group_template)
|
||||
]
|
||||
if (
|
||||
role == GroupRole.Building
|
||||
and GroupTask.StrikeTarget in template.tasks
|
||||
and faction_template.category not in self.building_set
|
||||
):
|
||||
# Special handling for strike targets. Skip if not supported by faction
|
||||
continue
|
||||
if faction_template.groups:
|
||||
self.templates.add_template(role, faction_template)
|
||||
continue
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
logging.info(f"{self.name} can not use template {template.name}")
|
||||
return sorted(air_defenses)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
||||
@@ -350,8 +206,13 @@ class Faction:
|
||||
]
|
||||
faction.missiles = [GroundUnitType.named(n) for n in json.get("missiles", [])]
|
||||
|
||||
faction.naval_units = [
|
||||
ShipUnitType.named(n) for n in json.get("naval_units", [])
|
||||
]
|
||||
|
||||
# This has to be loaded AFTER GroundUnitType and ShipUnitType to work properly
|
||||
faction.preset_groups = [
|
||||
UnitGroup.named(n) for n in json.get("preset_groups", [])
|
||||
ForceGroup.named(n) for n in json.get("preset_groups", [])
|
||||
]
|
||||
|
||||
faction.requirements = json.get("requirements", {})
|
||||
@@ -359,10 +220,6 @@ class Faction:
|
||||
faction.carrier_names = json.get("carrier_names", [])
|
||||
faction.helicopter_carrier_names = json.get("helicopter_carrier_names", [])
|
||||
|
||||
faction.naval_units = [
|
||||
ShipUnitType.named(n) for n in json.get("naval_units", [])
|
||||
]
|
||||
|
||||
faction.has_jtac = json.get("has_jtac", False)
|
||||
jtac_name = json.get("jtac_unit", None)
|
||||
if jtac_name is not None:
|
||||
@@ -394,6 +251,9 @@ class Faction:
|
||||
else:
|
||||
faction.building_set = DEFAULT_AVAILABLE_BUILDINGS
|
||||
|
||||
# Add required buildings for the game logic (e.g. ammo, factory..)
|
||||
faction.building_set.extend(REQUIRED_BUILDINGS)
|
||||
|
||||
# Load liveries override
|
||||
faction.liveries_overrides = {}
|
||||
liveries_overrides = json.get("liveries_overrides", {})
|
||||
@@ -403,9 +263,6 @@ class Faction:
|
||||
|
||||
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
|
||||
|
||||
# Templates
|
||||
faction.templates = GroundObjectTemplates()
|
||||
|
||||
return faction
|
||||
|
||||
@property
|
||||
@@ -419,44 +276,7 @@ class Faction:
|
||||
if unit.unit_class is unit_class:
|
||||
yield unit
|
||||
|
||||
def groups_for_role_and_task(
|
||||
self, group_role: GroupRole, group_task: Optional[GroupTask] = None
|
||||
) -> list[UnitGroup]:
|
||||
if group_role not in self.unit_groups:
|
||||
return []
|
||||
groups = []
|
||||
for unit_group in self.unit_groups[group_role]:
|
||||
if not group_task or group_task in unit_group.tasks:
|
||||
groups.append(unit_group)
|
||||
return groups
|
||||
|
||||
def groups_for_role_and_tasks(
|
||||
self, group_role: GroupRole, tasks: list[GroupTask]
|
||||
) -> list[UnitGroup]:
|
||||
groups = []
|
||||
for task in tasks:
|
||||
for group in self.groups_for_role_and_task(group_role, task):
|
||||
if group not in groups:
|
||||
groups.append(group)
|
||||
return groups
|
||||
|
||||
def random_group_for_role(self, group_role: GroupRole) -> Optional[UnitGroup]:
|
||||
unit_groups = self.groups_for_role_and_task(group_role)
|
||||
return random.choice(unit_groups) if unit_groups else None
|
||||
|
||||
def random_group_for_role_and_task(
|
||||
self, group_role: GroupRole, group_task: GroupTask
|
||||
) -> Optional[UnitGroup]:
|
||||
unit_groups = self.groups_for_role_and_task(group_role, group_task)
|
||||
return random.choice(unit_groups) if unit_groups else None
|
||||
|
||||
def random_group_for_role_and_tasks(
|
||||
self, group_role: GroupRole, tasks: list[GroupTask]
|
||||
) -> Optional[UnitGroup]:
|
||||
unit_groups = self.groups_for_role_and_tasks(group_role, tasks)
|
||||
return random.choice(unit_groups) if unit_groups else None
|
||||
|
||||
def _apply_mod_settings(self, mod_settings: ModSettings) -> None:
|
||||
def apply_mod_settings(self, mod_settings: ModSettings) -> None:
|
||||
# aircraft
|
||||
if not mod_settings.a4_skyhawk:
|
||||
self.remove_aircraft("A-4E-C")
|
||||
@@ -516,20 +336,20 @@ class Faction:
|
||||
self.remove_vehicle("KORNET")
|
||||
# high digit sams
|
||||
if not mod_settings.high_digit_sams:
|
||||
self.remove_presets("SA-10B/S-300PS")
|
||||
self.remove_presets("SA-12/S-300V")
|
||||
self.remove_presets("SA-20/S-300PMU-1")
|
||||
self.remove_presets("SA-20B/S-300PMU-2")
|
||||
self.remove_presets("SA-23/S-300VM")
|
||||
self.remove_presets("SA-17")
|
||||
self.remove_presets("KS-19")
|
||||
self.remove_preset("SA-10B/S-300PS")
|
||||
self.remove_preset("SA-12/S-300V")
|
||||
self.remove_preset("SA-20/S-300PMU-1")
|
||||
self.remove_preset("SA-20B/S-300PMU-2")
|
||||
self.remove_preset("SA-23/S-300VM")
|
||||
self.remove_preset("SA-17")
|
||||
self.remove_preset("KS-19")
|
||||
|
||||
def remove_aircraft(self, name: str) -> None:
|
||||
for i in self.aircrafts:
|
||||
if i.dcs_unit_type.id == name:
|
||||
self.aircrafts.remove(i)
|
||||
|
||||
def remove_presets(self, name: str) -> None:
|
||||
def remove_preset(self, name: str) -> None:
|
||||
for pg in self.preset_groups:
|
||||
if pg.name == name:
|
||||
self.preset_groups.remove(pg)
|
||||
|
||||
@@ -500,7 +500,7 @@ class Game:
|
||||
return False
|
||||
return True
|
||||
|
||||
def iads_considerate_culling(self, tgo: TheaterGroundObject[Any]) -> bool:
|
||||
def iads_considerate_culling(self, tgo: TheaterGroundObject) -> bool:
|
||||
if not self.settings.perf_do_not_cull_threatening_iads:
|
||||
return self.position_culled(tgo.position)
|
||||
else:
|
||||
|
||||
4
game/layout/__init__.py
Normal file
4
game/layout/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from layout import TheaterLayout
|
||||
from game.layout.layoutloader import LayoutLoader
|
||||
|
||||
LAYOUTS = LayoutLoader()
|
||||
268
game/layout/layout.py
Normal file
268
game/layout/layout.py
Normal file
@@ -0,0 +1,268 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from dcs import Point
|
||||
from dcs.unit import Unit
|
||||
from dcs.unittype import UnitType as DcsUnitType
|
||||
|
||||
from game.data.groups import GroupRole, GroupTask
|
||||
from game.data.units import UnitClass
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from game.theater.theatergroundobject import (
|
||||
SamGroundObject,
|
||||
EwrGroundObject,
|
||||
BuildingGroundObject,
|
||||
MissileSiteGroundObject,
|
||||
ShipGroundObject,
|
||||
CarrierGroundObject,
|
||||
LhaGroundObject,
|
||||
CoastalSiteGroundObject,
|
||||
VehicleGroupGroundObject,
|
||||
IadsGroundObject,
|
||||
)
|
||||
from game.theater.theatergroup import TheaterUnit
|
||||
from game.utils import Heading
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.factions.faction import Faction
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
from game.theater.controlpoint import ControlPoint
|
||||
|
||||
|
||||
class LayoutException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class LayoutUnit:
|
||||
"""The Position and Orientation of a single unit within the GroupLayout"""
|
||||
|
||||
name: str
|
||||
position: Point
|
||||
heading: int
|
||||
|
||||
@staticmethod
|
||||
def from_unit(unit: Unit) -> LayoutUnit:
|
||||
"""Creates a LayoutUnit from a DCS Unit"""
|
||||
return LayoutUnit(
|
||||
unit.name,
|
||||
Point(int(unit.position.x), int(unit.position.y)),
|
||||
int(unit.heading),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroupLayout:
|
||||
"""The Layout of a TheaterGroup"""
|
||||
|
||||
name: str
|
||||
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
|
||||
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)
|
||||
|
||||
# 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"""
|
||||
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:
|
||||
alternative_types.append(accessible_unit.dcs_unit_type)
|
||||
|
||||
if not unit_types and not alternative_types and not self.optional:
|
||||
raise LayoutException
|
||||
|
||||
return unit_types or alternative_types
|
||||
|
||||
@property
|
||||
def unit_counter(self) -> int:
|
||||
"""TODO Documentation"""
|
||||
default = len(self.units)
|
||||
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
|
||||
|
||||
@property
|
||||
def max_size(self) -> int:
|
||||
return len(self.units)
|
||||
|
||||
def generate_units(
|
||||
self, go: TheaterGroundObject, unit_type: Type[DcsUnitType], amount: int
|
||||
) -> list[TheaterUnit]:
|
||||
"""TODO Documentation"""
|
||||
return [
|
||||
TheaterUnit.from_template(i, unit_type, self.units[i], go)
|
||||
for i in range(amount)
|
||||
]
|
||||
|
||||
|
||||
class TheaterLayout:
|
||||
"""TODO Documentation"""
|
||||
|
||||
def __init__(self, name: str, role: GroupRole, description: str = "") -> None:
|
||||
self.name = name
|
||||
self.role = role
|
||||
self.description = description
|
||||
self.tasks: list[GroupTask] = [] # The supported tasks
|
||||
self.groups: list[GroupLayout] = []
|
||||
|
||||
# If the template is generic it will be used the generate the general
|
||||
# UnitGroups during faction initialization. Generic Groups allow to be mixed
|
||||
self.generic: bool = False
|
||||
|
||||
def usable_by_faction(self, faction: Faction) -> bool:
|
||||
# Special handling for Buildings
|
||||
if (
|
||||
isinstance(self, BuildingLayout)
|
||||
and self.category not in faction.building_set
|
||||
):
|
||||
return False
|
||||
|
||||
# Check if faction has at least 1 possible unit for non-optional groups
|
||||
try:
|
||||
return all(
|
||||
len(group.possible_types_for_faction(faction)) > 0
|
||||
for group in self.groups
|
||||
if not group.optional
|
||||
)
|
||||
except LayoutException:
|
||||
return False
|
||||
|
||||
def create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
control_point: ControlPoint,
|
||||
) -> TheaterGroundObject:
|
||||
"""TODO Documentation"""
|
||||
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])
|
||||
|
||||
|
||||
class AntiAirLayout(TheaterLayout):
|
||||
def create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
control_point: ControlPoint,
|
||||
) -> IadsGroundObject:
|
||||
|
||||
if GroupTask.EARLY_WARNING_RADAR in self.tasks:
|
||||
return EwrGroundObject(name, position, position.heading, control_point)
|
||||
elif any(tasking in self.tasks for tasking in GroupRole.AIR_DEFENSE.tasks):
|
||||
return SamGroundObject(name, position, position.heading, control_point)
|
||||
raise RuntimeError(
|
||||
f" No Template for AntiAir tasking ({', '.join(task.description for task in self.tasks)})"
|
||||
)
|
||||
|
||||
|
||||
class BuildingLayout(TheaterLayout):
|
||||
def create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
control_point: ControlPoint,
|
||||
) -> BuildingGroundObject:
|
||||
return BuildingGroundObject(
|
||||
name,
|
||||
self.category,
|
||||
position,
|
||||
Heading.from_degrees(0),
|
||||
control_point,
|
||||
self.category == "fob",
|
||||
)
|
||||
|
||||
@property
|
||||
def category(self) -> str:
|
||||
for task in self.tasks:
|
||||
if task not in [GroupTask.STRIKE_TARGET, GroupTask.OFFSHORE_STRIKE_TARGET]:
|
||||
return task.description.lower()
|
||||
raise RuntimeError(f"Building Template {self.name} has no building category")
|
||||
|
||||
|
||||
class NavalLayout(TheaterLayout):
|
||||
def create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
control_point: ControlPoint,
|
||||
) -> TheaterGroundObject:
|
||||
if GroupTask.NAVY in self.tasks:
|
||||
return ShipGroundObject(name, position, control_point)
|
||||
elif GroupTask.AIRCRAFT_CARRIER in self.tasks:
|
||||
return CarrierGroundObject(name, control_point)
|
||||
elif GroupTask.HELICOPTER_CARRIER in self.tasks:
|
||||
return LhaGroundObject(name, control_point)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DefensesLayout(TheaterLayout):
|
||||
def create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
control_point: ControlPoint,
|
||||
) -> TheaterGroundObject:
|
||||
if GroupTask.MISSILE in self.tasks:
|
||||
return MissileSiteGroundObject(
|
||||
name, position, position.heading, control_point
|
||||
)
|
||||
elif GroupTask.COASTAL in self.tasks:
|
||||
return CoastalSiteGroundObject(
|
||||
name, position, control_point, position.heading
|
||||
)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class GroundForceLayout(TheaterLayout):
|
||||
def create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
control_point: ControlPoint,
|
||||
) -> TheaterGroundObject:
|
||||
return VehicleGroupGroundObject(name, position, position.heading, control_point)
|
||||
203
game/layout/layoutloader.py
Normal file
203
game/layout/layoutloader.py
Normal file
@@ -0,0 +1,203 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import pickle
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
from typing import Iterator
|
||||
|
||||
import dcs
|
||||
import yaml
|
||||
from dcs import Point
|
||||
from dcs.unitgroup import StaticGroup
|
||||
|
||||
from game import persistency
|
||||
from game.data.groups import GroupRole, GroupTask
|
||||
from game.layout.layout import (
|
||||
TheaterLayout,
|
||||
GroupLayout,
|
||||
LayoutUnit,
|
||||
AntiAirLayout,
|
||||
BuildingLayout,
|
||||
NavalLayout,
|
||||
GroundForceLayout,
|
||||
DefensesLayout,
|
||||
)
|
||||
from game.layout.layoutmapping import GroupLayoutMapping, LayoutMapping
|
||||
from game.profiling import logged_duration
|
||||
from game.version import VERSION
|
||||
|
||||
TEMPLATE_DIR = "resources/layouts/"
|
||||
TEMPLATE_DUMP = "Liberation/layouts.p"
|
||||
|
||||
TEMPLATE_TYPES = {
|
||||
GroupRole.AIR_DEFENSE: AntiAirLayout,
|
||||
GroupRole.BUILDING: BuildingLayout,
|
||||
GroupRole.NAVAL: NavalLayout,
|
||||
GroupRole.GROUND_FORCE: GroundForceLayout,
|
||||
GroupRole.DEFENSES: DefensesLayout,
|
||||
}
|
||||
|
||||
|
||||
class LayoutLoader:
|
||||
# list of layouts per category. e.g. AA or similar
|
||||
_templates: dict[str, TheaterLayout] = {}
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._templates = {}
|
||||
|
||||
def initialize(self) -> None:
|
||||
if not self._templates:
|
||||
with logged_duration("Loading layouts"):
|
||||
self.load_templates()
|
||||
|
||||
@property
|
||||
def layouts(self) -> Iterator[TheaterLayout]:
|
||||
self.initialize()
|
||||
yield from self._templates.values()
|
||||
|
||||
def load_templates(self) -> None:
|
||||
"""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
|
||||
if file.is_file():
|
||||
# Load from pickle if existing
|
||||
with file.open("rb") as f:
|
||||
try:
|
||||
version, self._templates = 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.")
|
||||
# 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]] = {}
|
||||
with logged_duration("Parsing mapping yamls"):
|
||||
for file in Path(TEMPLATE_DIR).rglob("*.yaml"):
|
||||
if not file.is_file():
|
||||
continue
|
||||
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]
|
||||
|
||||
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)
|
||||
|
||||
logging.info(f"Imported {len(self._templates)} layouts")
|
||||
self._dump_templates()
|
||||
|
||||
def _dump_templates(self) -> None:
|
||||
file = Path(persistency.base_path()) / TEMPLATE_DUMP
|
||||
dump = (VERSION, self._templates)
|
||||
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()
|
||||
with logged_duration(f"Parsing {miz}"):
|
||||
# The load_file takes a lot of time to compute. That's why the layouts
|
||||
# are written to a pickle and can be reloaded from the ui
|
||||
# Example the whole routine: 0:00:00.934417,
|
||||
# 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,
|
||||
):
|
||||
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
|
||||
|
||||
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,
|
||||
)
|
||||
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)
|
||||
|
||||
def by_name(self, template_name: str) -> Iterator[TheaterLayout]:
|
||||
for template in self.layouts:
|
||||
if template.name == template_name:
|
||||
yield template
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
156
game/layout/layoutmapping.py
Normal file
156
game/layout/layoutmapping.py
Normal file
@@ -0,0 +1,156 @@
|
||||
from __future__ import annotations
|
||||
|
||||
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
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroupLayoutMapping:
|
||||
# The group name used in the template.miz
|
||||
name: str
|
||||
|
||||
# Defines if the group is required for the template or can be skipped
|
||||
optional: bool = False
|
||||
|
||||
# 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.
|
||||
unit_count: list[int] = field(default_factory=list)
|
||||
|
||||
# All unit types the template supports.
|
||||
unit_types: list[Type[DcsUnitType]] = field(default_factory=list)
|
||||
|
||||
# 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
|
||||
# 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
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d: dict[str, Any]) -> GroupLayoutMapping:
|
||||
optional = d["optional"] if "optional" in d else False
|
||||
statics = d["statics"] if "statics" in d else []
|
||||
unit_count = d["unit_count"] if "unit_count" in d else []
|
||||
unit_types = []
|
||||
if "unit_types" in d:
|
||||
for u in d["unit_types"]:
|
||||
unit_type = db.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
|
||||
else []
|
||||
)
|
||||
return GroupLayoutMapping(
|
||||
d["name"],
|
||||
optional,
|
||||
statics,
|
||||
group,
|
||||
unit_count,
|
||||
unit_types,
|
||||
unit_classes,
|
||||
alternative_classes,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LayoutMapping:
|
||||
# The name of the Template
|
||||
name: str
|
||||
|
||||
# An optional description to give more information about the template
|
||||
description: str
|
||||
|
||||
# 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]
|
||||
|
||||
# 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
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d: dict[str, Any], file_name: str) -> LayoutMapping:
|
||||
groups = [GroupLayoutMapping.from_dict(group) for group in d["groups"]]
|
||||
description = d["description"] if "description" in d else ""
|
||||
generic = d["generic"] if "generic" in d else False
|
||||
layout_file = (
|
||||
d["layout_file"] if "layout_file" in d else file_name.replace("yaml", "miz")
|
||||
)
|
||||
tasks = [GroupTask.by_description(task) for task in d["tasks"]]
|
||||
return LayoutMapping(
|
||||
d["name"],
|
||||
description,
|
||||
generic,
|
||||
GroupRole(d["role"]),
|
||||
tasks,
|
||||
groups,
|
||||
layout_file,
|
||||
)
|
||||
@@ -12,7 +12,7 @@ from dcs.unitgroup import FlyingGroup
|
||||
from game.ato import Flight, FlightWaypoint
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.missiongenerator.airsupport import AirSupport
|
||||
from game.theater import MissionTarget, GroundUnit
|
||||
from game.theater import MissionTarget, TheaterUnit
|
||||
|
||||
TARGET_WAYPOINTS = (
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
@@ -82,7 +82,7 @@ class PydcsWaypointBuilder:
|
||||
return False
|
||||
|
||||
def register_special_waypoints(
|
||||
self, targets: Iterable[Union[MissionTarget, GroundUnit]]
|
||||
self, targets: Iterable[Union[MissionTarget, TheaterUnit]]
|
||||
) -> None:
|
||||
"""Create special target waypoints for various aircraft"""
|
||||
for i, t in enumerate(targets):
|
||||
|
||||
@@ -221,7 +221,7 @@ class FlotGenerator:
|
||||
if self.game.settings.manpads:
|
||||
# 50% of armored units protected by manpad
|
||||
if random.choice([True, False]):
|
||||
manpads = list(faction.infantry_with_class(UnitClass.Manpad))
|
||||
manpads = list(faction.infantry_with_class(UnitClass.MANPAD))
|
||||
if manpads:
|
||||
u = random.choices(
|
||||
manpads, weights=[m.spawn_weight for m in manpads]
|
||||
@@ -237,10 +237,10 @@ class FlotGenerator:
|
||||
)
|
||||
return
|
||||
|
||||
possible_infantry_units = set(faction.infantry_with_class(UnitClass.Infantry))
|
||||
possible_infantry_units = set(faction.infantry_with_class(UnitClass.INFANTRY))
|
||||
if self.game.settings.manpads:
|
||||
possible_infantry_units |= set(
|
||||
faction.infantry_with_class(UnitClass.Manpad)
|
||||
faction.infantry_with_class(UnitClass.MANPAD)
|
||||
)
|
||||
if not possible_infantry_units:
|
||||
return
|
||||
|
||||
@@ -40,7 +40,7 @@ from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.data.alic import AlicCodes
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.radio.radios import RadioFrequency
|
||||
from game.theater import ConflictTheater, LatLon, TheaterGroundObject, GroundUnit
|
||||
from game.theater import ConflictTheater, LatLon, TheaterGroundObject, TheaterUnit
|
||||
from game.theater.bullseye import Bullseye
|
||||
from game.utils import Distance, UnitSystem, meters, mps, pounds
|
||||
from game.weather import Weather
|
||||
@@ -607,14 +607,14 @@ class SeadTaskPage(KneeboardPage):
|
||||
self.theater = theater
|
||||
|
||||
@property
|
||||
def target_units(self) -> Iterator[GroundUnit]:
|
||||
def target_units(self) -> Iterator[TheaterUnit]:
|
||||
if isinstance(self.flight.package.target, TheaterGroundObject):
|
||||
yield from self.flight.package.target.strike_targets
|
||||
|
||||
@staticmethod
|
||||
def alic_for(unit_type: str) -> str:
|
||||
def alic_for(unit: TheaterUnit) -> str:
|
||||
try:
|
||||
return str(AlicCodes.code_for(unit_type))
|
||||
return str(AlicCodes.code_for(unit))
|
||||
except KeyError:
|
||||
return ""
|
||||
|
||||
@@ -634,13 +634,13 @@ class SeadTaskPage(KneeboardPage):
|
||||
|
||||
writer.write(path)
|
||||
|
||||
def target_info_row(self, unit: GroundUnit) -> List[str]:
|
||||
def target_info_row(self, unit: TheaterUnit) -> List[str]:
|
||||
ll = self.theater.point_to_ll(unit.position)
|
||||
unit_type = unit_type_from_name(unit.type)
|
||||
unit_type = unit.type
|
||||
name = unit.name if unit_type is None else unit_type.name
|
||||
return [
|
||||
name,
|
||||
self.alic_for(unit.type),
|
||||
self.alic_for(unit),
|
||||
ll.format_dms(include_decimal_seconds=True),
|
||||
]
|
||||
|
||||
|
||||
@@ -58,16 +58,14 @@ from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
|
||||
from game.dcs.helpers import static_type_from_name, unit_type_from_name
|
||||
from game.radio.radios import RadioFrequency, RadioRegistry
|
||||
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
|
||||
from game.theater import ControlPoint, TheaterGroundObject
|
||||
from game.theater import ControlPoint, TheaterGroundObject, TheaterUnit
|
||||
from game.theater.theatergroundobject import (
|
||||
CarrierGroundObject,
|
||||
GenericCarrierGroundObject,
|
||||
LhaGroundObject,
|
||||
MissileSiteGroundObject,
|
||||
GroundGroup,
|
||||
GroundUnit,
|
||||
SceneryGroundUnit,
|
||||
)
|
||||
from game.theater.theatergroup import SceneryUnit, TheaterGroup
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import Heading, feet, knots, mps
|
||||
from gen.runways import RunwayData
|
||||
@@ -100,95 +98,114 @@ class GroundObjectGenerator:
|
||||
def culled(self) -> bool:
|
||||
return self.game.iads_considerate_culling(self.ground_object)
|
||||
|
||||
def generate(self, unique_name: bool = True) -> None:
|
||||
def generate(self) -> None:
|
||||
if self.culled:
|
||||
return
|
||||
|
||||
for group in self.ground_object.groups:
|
||||
if not group.units:
|
||||
logging.warning(f"Found empty group in {self.ground_object}")
|
||||
continue
|
||||
group_name = group.group_name if unique_name else group.name
|
||||
moving_group: Optional[MovingGroup[Any]] = None
|
||||
for i, unit in enumerate(group.units):
|
||||
if isinstance(unit, SceneryGroundUnit):
|
||||
# Special handling for scenery objects:
|
||||
# Only create a trigger zone and no "real" dcs unit
|
||||
self.add_trigger_zone_for_scenery(unit)
|
||||
continue
|
||||
vehicle_units = []
|
||||
ship_units = []
|
||||
# Split the different unit types to be compliant to dcs limitation
|
||||
for unit in group.units:
|
||||
if unit.is_static:
|
||||
# A Static unit has to be a single static group
|
||||
self.create_static_group(unit)
|
||||
elif unit.is_vehicle and unit.alive:
|
||||
# All alive Vehicles
|
||||
vehicle_units.append(unit)
|
||||
elif unit.is_ship and unit.alive:
|
||||
# All alive Ships
|
||||
ship_units.append(unit)
|
||||
if vehicle_units:
|
||||
self.create_vehicle_group(group.group_name, vehicle_units)
|
||||
if ship_units:
|
||||
self.create_ship_group(group.group_name, ship_units)
|
||||
|
||||
# Only skip dead units after trigger zone for scenery created!
|
||||
if not unit.alive:
|
||||
continue
|
||||
def create_vehicle_group(
|
||||
self, group_name: str, units: list[TheaterUnit]
|
||||
) -> VehicleGroup:
|
||||
vehicle_group: Optional[VehicleGroup] = None
|
||||
for unit in units:
|
||||
assert issubclass(unit.type, VehicleType)
|
||||
if vehicle_group is None:
|
||||
vehicle_group = self.m.vehicle_group(
|
||||
self.country,
|
||||
group_name,
|
||||
unit.type,
|
||||
position=unit.position,
|
||||
heading=unit.position.heading.degrees,
|
||||
)
|
||||
vehicle_group.units[0].player_can_drive = True
|
||||
self.enable_eplrs(vehicle_group, unit.type)
|
||||
vehicle_group.units[0].name = unit.unit_name
|
||||
self.set_alarm_state(vehicle_group)
|
||||
else:
|
||||
vehicle_unit = Vehicle(
|
||||
self.m.next_unit_id(),
|
||||
unit.unit_name,
|
||||
unit.type.id,
|
||||
)
|
||||
vehicle_unit.player_can_drive = True
|
||||
vehicle_unit.position = unit.position
|
||||
vehicle_unit.heading = unit.position.heading.degrees
|
||||
vehicle_group.add_unit(vehicle_unit)
|
||||
self._register_theater_unit(unit, vehicle_group.units[-1])
|
||||
if vehicle_group is None:
|
||||
raise RuntimeError(f"Error creating VehicleGroup for {group_name}")
|
||||
return vehicle_group
|
||||
|
||||
unit_type = unit_type_from_name(unit.type)
|
||||
if not unit_type:
|
||||
raise RuntimeError(
|
||||
f"Unit type {unit.type} is not a valid dcs unit type"
|
||||
)
|
||||
def create_ship_group(
|
||||
self,
|
||||
group_name: str,
|
||||
units: list[TheaterUnit],
|
||||
frequency: Optional[RadioFrequency] = None,
|
||||
) -> ShipGroup:
|
||||
ship_group: Optional[ShipGroup] = None
|
||||
for unit in units:
|
||||
assert issubclass(unit.type, ShipType)
|
||||
if ship_group is None:
|
||||
ship_group = self.m.ship_group(
|
||||
self.country,
|
||||
group_name,
|
||||
unit.type,
|
||||
position=unit.position,
|
||||
heading=unit.position.heading.degrees,
|
||||
)
|
||||
if frequency:
|
||||
ship_group.set_frequency(frequency.hertz)
|
||||
ship_group.units[0].name = unit.unit_name
|
||||
self.set_alarm_state(ship_group)
|
||||
else:
|
||||
ship_unit = Ship(
|
||||
self.m.next_unit_id(),
|
||||
unit.unit_name,
|
||||
unit.type,
|
||||
)
|
||||
if frequency:
|
||||
ship_unit.set_frequency(frequency.hertz)
|
||||
ship_unit.position = unit.position
|
||||
ship_unit.heading = unit.position.heading.degrees
|
||||
ship_group.add_unit(ship_unit)
|
||||
self._register_theater_unit(unit, ship_group.units[-1])
|
||||
if ship_group is None:
|
||||
raise RuntimeError(f"Error creating ShipGroup for {group_name}")
|
||||
return ship_group
|
||||
|
||||
unit_name = unit.unit_name if unique_name else unit.name
|
||||
if moving_group is None or group.static_group:
|
||||
# First unit of the group will create the dcs group
|
||||
if issubclass(unit_type, VehicleType):
|
||||
moving_group = self.m.vehicle_group(
|
||||
self.country,
|
||||
group_name,
|
||||
unit_type,
|
||||
position=unit.position,
|
||||
heading=unit.position.heading.degrees,
|
||||
)
|
||||
moving_group.units[0].player_can_drive = True
|
||||
self.enable_eplrs(moving_group, unit_type)
|
||||
elif issubclass(unit_type, ShipType):
|
||||
moving_group = self.m.ship_group(
|
||||
self.country,
|
||||
group_name,
|
||||
unit_type,
|
||||
position=unit.position,
|
||||
heading=unit.position.heading.degrees,
|
||||
)
|
||||
elif issubclass(unit_type, StaticType):
|
||||
static_group = self.m.static_group(
|
||||
country=self.country,
|
||||
name=unit_name,
|
||||
_type=unit_type,
|
||||
position=unit.position,
|
||||
heading=unit.position.heading.degrees,
|
||||
dead=not unit.alive,
|
||||
)
|
||||
self._register_ground_unit(unit, static_group.units[0])
|
||||
continue
|
||||
def create_static_group(self, unit: TheaterUnit) -> None:
|
||||
if isinstance(unit, SceneryUnit):
|
||||
# Special handling for scenery objects:
|
||||
# Only create a trigger zone and no "real" dcs unit
|
||||
self.add_trigger_zone_for_scenery(unit)
|
||||
return
|
||||
|
||||
if moving_group:
|
||||
moving_group.units[0].name = unit_name
|
||||
self.set_alarm_state(moving_group)
|
||||
self._register_ground_unit(unit, moving_group.units[0])
|
||||
else:
|
||||
raise RuntimeError("DCS Group creation failed")
|
||||
else:
|
||||
# Additional Units in the group
|
||||
dcs_unit: Optional[Unit] = None
|
||||
if issubclass(unit_type, VehicleType):
|
||||
dcs_unit = Vehicle(
|
||||
self.m.next_unit_id(),
|
||||
unit_name,
|
||||
unit.type,
|
||||
)
|
||||
dcs_unit.player_can_drive = True
|
||||
elif issubclass(unit_type, ShipType):
|
||||
dcs_unit = Ship(
|
||||
self.m.next_unit_id(),
|
||||
unit_name,
|
||||
unit_type,
|
||||
)
|
||||
if dcs_unit:
|
||||
dcs_unit.position = unit.position
|
||||
dcs_unit.heading = unit.position.heading.degrees
|
||||
moving_group.add_unit(dcs_unit)
|
||||
self._register_ground_unit(unit, dcs_unit)
|
||||
else:
|
||||
raise RuntimeError("DCS Unit creation failed")
|
||||
static_group = self.m.static_group(
|
||||
country=self.country,
|
||||
name=unit.unit_name,
|
||||
_type=unit.type,
|
||||
position=unit.position,
|
||||
heading=unit.position.heading.degrees,
|
||||
dead=not unit.alive,
|
||||
)
|
||||
self._register_theater_unit(unit, static_group.units[0])
|
||||
|
||||
@staticmethod
|
||||
def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
|
||||
@@ -201,14 +218,14 @@ class GroundObjectGenerator:
|
||||
else:
|
||||
group.points[0].tasks.append(OptAlarmState(1))
|
||||
|
||||
def _register_ground_unit(
|
||||
def _register_theater_unit(
|
||||
self,
|
||||
ground_unit: GroundUnit,
|
||||
theater_unit: TheaterUnit,
|
||||
dcs_unit: Unit,
|
||||
) -> None:
|
||||
self.unit_map.add_ground_object_mapping(ground_unit, dcs_unit)
|
||||
self.unit_map.add_theater_unit_mapping(theater_unit, dcs_unit)
|
||||
|
||||
def add_trigger_zone_for_scenery(self, scenery: SceneryGroundUnit) -> None:
|
||||
def add_trigger_zone_for_scenery(self, scenery: SceneryUnit) -> None:
|
||||
# Align the trigger zones to the faction color on the DCS briefing/F10 map.
|
||||
color = (
|
||||
{1: 0.2, 2: 0.7, 3: 1, 4: 0.15}
|
||||
@@ -265,7 +282,7 @@ class MissileSiteGenerator(GroundObjectGenerator):
|
||||
# culled despite being a threat.
|
||||
return False
|
||||
|
||||
def generate(self, unique_name: bool = True) -> None:
|
||||
def generate(self) -> None:
|
||||
super(MissileSiteGenerator, self).generate()
|
||||
# Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now)
|
||||
# TODO : Should be pre-planned ?
|
||||
@@ -347,7 +364,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
||||
self.icls_alloc = icls_alloc
|
||||
self.runways = runways
|
||||
|
||||
def generate(self, unique_name: bool = True) -> None:
|
||||
def generate(self) -> None:
|
||||
|
||||
# This can also be refactored as the general generation was updated
|
||||
atc = self.radio_registry.alloc_uhf()
|
||||
@@ -357,40 +374,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
||||
logging.warning(f"Found empty carrier group in {self.control_point}")
|
||||
continue
|
||||
|
||||
# Correct unit type for the carrier.
|
||||
# This is only used for the super carrier setting
|
||||
unit_type = (
|
||||
self.get_carrier_type(group)
|
||||
if g_id == 0
|
||||
else ship_map[group.units[0].type]
|
||||
)
|
||||
|
||||
ship_group = self.m.ship_group(
|
||||
self.country,
|
||||
group.group_name if unique_name else group.name,
|
||||
unit_type,
|
||||
position=group.units[0].position,
|
||||
heading=group.units[0].position.heading.degrees,
|
||||
)
|
||||
|
||||
ship_group.set_frequency(atc.hertz)
|
||||
ship_group.units[0].name = (
|
||||
group.units[0].unit_name if unique_name else group.units[0].name
|
||||
)
|
||||
self._register_ground_unit(group.units[0], ship_group.units[0])
|
||||
|
||||
for unit in group.units[1:]:
|
||||
ship = Ship(
|
||||
self.m.next_unit_id(),
|
||||
unit.unit_name if unique_name else unit.name,
|
||||
unit_type_from_name(unit.type),
|
||||
)
|
||||
ship.position.x = unit.position.x
|
||||
ship.position.y = unit.position.y
|
||||
ship.heading = unit.position.heading.degrees
|
||||
ship.set_frequency(atc.hertz)
|
||||
ship_group.add_unit(ship)
|
||||
self._register_ground_unit(unit, ship)
|
||||
ship_group = self.create_ship_group(group.group_name, group.units, atc)
|
||||
|
||||
# Always steam into the wind, even if the carrier is being moved.
|
||||
# There are multiple unsimulated hours between turns, so we can
|
||||
@@ -400,19 +384,24 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
||||
|
||||
# Set Carrier Specific Options
|
||||
if g_id == 0:
|
||||
# Correct unit type for the carrier.
|
||||
# This is only used for the super carrier setting
|
||||
ship_group.units[0].type = self.get_carrier_type(group).id
|
||||
tacan = self.tacan_registry.alloc_for_band(
|
||||
TacanBand.X, TacanUsage.TransmitReceive
|
||||
)
|
||||
tacan_callsign = self.tacan_callsign()
|
||||
icls = next(self.icls_alloc)
|
||||
|
||||
self.activate_beacons(ship_group, tacan, tacan_callsign, icls)
|
||||
self.add_runway_data(
|
||||
brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls
|
||||
)
|
||||
|
||||
def get_carrier_type(self, group: GroundGroup) -> Type[ShipType]:
|
||||
return ship_map[group.units[0].type]
|
||||
def get_carrier_type(self, group: TheaterGroup) -> Type[ShipType]:
|
||||
carrier_type = group.units[0].type
|
||||
if issubclass(carrier_type, ShipType):
|
||||
return carrier_type
|
||||
raise RuntimeError(f"First unit of TGO {group.name} is no Ship")
|
||||
|
||||
def steam_into_wind(self, group: ShipGroup) -> Optional[Heading]:
|
||||
wind = self.game.conditions.weather.wind.at_0m
|
||||
@@ -479,7 +468,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
||||
class CarrierGenerator(GenericCarrierGenerator):
|
||||
"""Generator for CV(N) groups."""
|
||||
|
||||
def get_carrier_type(self, group: GroundGroup) -> Type[ShipType]:
|
||||
def get_carrier_type(self, group: TheaterGroup) -> Type[ShipType]:
|
||||
unit_type = super().get_carrier_type(group)
|
||||
if self.game.settings.supercarrier:
|
||||
unit_type = self.upgrade_to_supercarrier(unit_type, self.control_point.name)
|
||||
|
||||
@@ -176,7 +176,7 @@ class ProcurementAi:
|
||||
worst_fulfillment = fulfillment
|
||||
worst_balanced = unit_class
|
||||
if worst_balanced is None:
|
||||
return UnitClass.Tank
|
||||
return UnitClass.TANK
|
||||
return worst_balanced
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -132,7 +132,7 @@ class MissionResultsProcessor:
|
||||
@staticmethod
|
||||
def commit_ground_losses(debriefing: Debriefing) -> None:
|
||||
for ground_object_loss in debriefing.ground_object_losses:
|
||||
ground_object_loss.ground_unit.kill()
|
||||
ground_object_loss.theater_unit.kill()
|
||||
for scenery_object_loss in debriefing.scenery_object_losses:
|
||||
scenery_object_loss.ground_unit.kill()
|
||||
|
||||
|
||||
@@ -20,13 +20,11 @@ from typing import (
|
||||
Set,
|
||||
TYPE_CHECKING,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.ships import Forrestal, KUZNECOW, LHA_Tarawa, Stennis, Type_071
|
||||
from dcs.terrain.terrain import Airport, ParkingSlot
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import ShipGroup, StaticGroup
|
||||
|
||||
from game.dcs.helpers import unit_type_from_name
|
||||
@@ -39,14 +37,10 @@ from gen.runways import RunwayAssigner, RunwayData
|
||||
from .base import Base
|
||||
from .missiontarget import MissionTarget
|
||||
from .theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
GenericCarrierGroundObject,
|
||||
TheaterGroundObject,
|
||||
BuildingGroundObject,
|
||||
CarrierGroundObject,
|
||||
LhaGroundObject,
|
||||
GroundUnit,
|
||||
)
|
||||
from .theatergroup import TheaterUnit
|
||||
from ..ato.starttype import StartType
|
||||
from ..data.units import UnitClass
|
||||
from ..dcs.aircrafttype import AircraftType
|
||||
@@ -525,8 +519,8 @@ class ControlPoint(MissionTarget, ABC):
|
||||
for group in g.groups:
|
||||
for u in group.units:
|
||||
if u.unit_type and u.unit_type.unit_class in [
|
||||
UnitClass.AircraftCarrier,
|
||||
UnitClass.HelicopterCarrier,
|
||||
UnitClass.AIRCRAFT_CARRIER,
|
||||
UnitClass.HELICOPTER_CARRIER,
|
||||
]:
|
||||
return group.group_name
|
||||
return None
|
||||
@@ -816,28 +810,26 @@ class ControlPoint(MissionTarget, ABC):
|
||||
return self.front_line_capacity_with(self.active_ammo_depots_count)
|
||||
|
||||
@property
|
||||
def all_ammo_depots(self) -> Iterator[BuildingGroundObject]:
|
||||
def all_ammo_depots(self) -> Iterator[TheaterGroundObject]:
|
||||
for tgo in self.connected_objectives:
|
||||
if not tgo.is_ammo_depot:
|
||||
continue
|
||||
assert isinstance(tgo, BuildingGroundObject)
|
||||
yield tgo
|
||||
|
||||
@property
|
||||
def active_ammo_depots(self) -> Iterator[BuildingGroundObject]:
|
||||
for tgo in self.all_ammo_depots:
|
||||
if not tgo.is_dead:
|
||||
if tgo.is_ammo_depot:
|
||||
yield tgo
|
||||
|
||||
def ammo_depot_count(self, alive_only: bool = False) -> int:
|
||||
return sum(
|
||||
ammo_depot.alive_unit_count if alive_only else ammo_depot.unit_count
|
||||
for ammo_depot in self.all_ammo_depots
|
||||
)
|
||||
|
||||
@property
|
||||
def active_ammo_depots_count(self) -> int:
|
||||
"""Return the number of available ammo depots"""
|
||||
return len(list(self.active_ammo_depots))
|
||||
return self.ammo_depot_count(True)
|
||||
|
||||
@property
|
||||
def total_ammo_depots_count(self) -> int:
|
||||
"""Return the number of ammo depots, including dead ones"""
|
||||
return len(list(self.all_ammo_depots))
|
||||
return self.ammo_depot_count()
|
||||
|
||||
@property
|
||||
def active_fuel_depots_count(self) -> int:
|
||||
@@ -856,7 +848,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
return len([obj for obj in self.connected_objectives if obj.category == "fuel"])
|
||||
|
||||
@property
|
||||
def strike_targets(self) -> list[GroundUnit]:
|
||||
def strike_targets(self) -> list[TheaterUnit]:
|
||||
return []
|
||||
|
||||
@property
|
||||
@@ -1008,7 +1000,7 @@ class NavalControlPoint(ControlPoint, ABC):
|
||||
# while its escorts are still alive.
|
||||
for group in self.find_main_tgo().groups:
|
||||
for u in group.units:
|
||||
if unit_type_from_name(u.type) in [
|
||||
if u.type in [
|
||||
Forrestal,
|
||||
Stennis,
|
||||
LHA_Tarawa,
|
||||
|
||||
@@ -8,7 +8,7 @@ from dcs.unit import Unit
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.theater.theatergroundobject import GroundUnit
|
||||
from game.theater import TheaterUnit
|
||||
|
||||
|
||||
class MissionTarget:
|
||||
@@ -47,5 +47,5 @@ class MissionTarget:
|
||||
]
|
||||
|
||||
@property
|
||||
def strike_targets(self) -> list[GroundUnit]:
|
||||
def strike_targets(self) -> list[TheaterUnit]:
|
||||
return []
|
||||
|
||||
@@ -6,19 +6,18 @@ from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
import dcs.statics
|
||||
|
||||
from game import Game
|
||||
from game.factions.faction import Faction
|
||||
from game.scenery_group import SceneryGroup
|
||||
from game.theater import PointWithHeading
|
||||
from game.theater.theatergroundobject import (
|
||||
AirDefenseRange,
|
||||
BuildingGroundObject,
|
||||
SceneryGroundUnit,
|
||||
GroundGroup,
|
||||
)
|
||||
from .theatergroup import SceneryUnit, TheaterGroup
|
||||
from game.utils import Heading
|
||||
from game.version import VERSION
|
||||
from gen.templates import GroundObjectTemplates, GroundObjectTemplate
|
||||
from gen.naming import namegen
|
||||
from . import (
|
||||
ConflictTheater,
|
||||
@@ -28,9 +27,9 @@ from . import (
|
||||
OffMapSpawn,
|
||||
)
|
||||
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
|
||||
from ..data.units import UnitClass
|
||||
from ..data.groups import GroupRole, GroupTask, ROLE_TASKINGS
|
||||
from ..dcs.unitgroup import UnitGroup
|
||||
from ..data.groups import GroupRole, GroupTask
|
||||
from ..armedforces.forcegroup import ForceGroup
|
||||
from ..armedforces.armedforces import ArmedForces
|
||||
from ..profiling import logged_duration
|
||||
from ..settings import Settings
|
||||
|
||||
@@ -77,9 +76,8 @@ class GameGenerator:
|
||||
self.air_wing_config = air_wing_config
|
||||
self.settings = settings
|
||||
self.generator_settings = generator_settings
|
||||
|
||||
with logged_duration(f"Initializing faction and templates"):
|
||||
self.initialize_factions(mod_settings)
|
||||
self.player.apply_mod_settings(mod_settings)
|
||||
self.enemy.apply_mod_settings(mod_settings)
|
||||
|
||||
def generate(self) -> Game:
|
||||
with logged_duration("TGO population"):
|
||||
@@ -126,12 +124,6 @@ class GameGenerator:
|
||||
for cp in to_remove:
|
||||
self.theater.controlpoints.remove(cp)
|
||||
|
||||
def initialize_factions(self, mod_settings: ModSettings) -> None:
|
||||
with logged_duration("Loading Templates from mapping"):
|
||||
templates = GroundObjectTemplates.from_folder("resources/templates/")
|
||||
self.player.initialize(templates, mod_settings)
|
||||
self.enemy.initialize(templates, mod_settings)
|
||||
|
||||
|
||||
class ControlPointGroundObjectGenerator:
|
||||
def __init__(
|
||||
@@ -152,35 +144,30 @@ class ControlPointGroundObjectGenerator:
|
||||
def faction(self) -> Faction:
|
||||
return self.game.coalition_for(self.control_point.captured).faction
|
||||
|
||||
@property
|
||||
def armed_forces(self) -> ArmedForces:
|
||||
return self.game.coalition_for(self.control_point.captured).armed_forces
|
||||
|
||||
def generate(self) -> bool:
|
||||
self.control_point.connected_objectives = []
|
||||
self.generate_navy()
|
||||
return True
|
||||
|
||||
def generate_random_ground_object(
|
||||
self, unit_groups: list[UnitGroup], position: PointWithHeading
|
||||
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: UnitGroup, position: PointWithHeading
|
||||
self, unit_group: ForceGroup, position: PointWithHeading
|
||||
) -> None:
|
||||
try:
|
||||
with logged_duration(
|
||||
f"Ground Object generation for unit_group "
|
||||
f"{unit_group.name} ({unit_group.role.value})"
|
||||
):
|
||||
ground_object = unit_group.generate(
|
||||
namegen.random_objective_name(),
|
||||
position,
|
||||
self.control_point,
|
||||
self.game,
|
||||
)
|
||||
self.control_point.connected_objectives.append(ground_object)
|
||||
except NotImplementedError:
|
||||
logging.error("Template Generator not implemented yet")
|
||||
except IndexError:
|
||||
logging.error(f"No templates to generate object from {unit_group.name}")
|
||||
ground_object = unit_group.generate(
|
||||
namegen.random_objective_name(),
|
||||
position,
|
||||
self.control_point,
|
||||
self.game,
|
||||
)
|
||||
self.control_point.connected_objectives.append(ground_object)
|
||||
|
||||
def generate_navy(self) -> None:
|
||||
skip_player_navy = self.generator_settings.no_player_navy
|
||||
@@ -190,11 +177,9 @@ class ControlPointGroundObjectGenerator:
|
||||
if not self.control_point.captured and skip_enemy_navy:
|
||||
return
|
||||
for position in self.control_point.preset_locations.ships:
|
||||
unit_group = self.faction.random_group_for_role_and_task(
|
||||
GroupRole.Naval, GroupTask.Navy
|
||||
)
|
||||
unit_group = self.armed_forces.random_group_for_task(GroupTask.NAVY)
|
||||
if not unit_group:
|
||||
logging.error(f"{self.faction_name} has no UnitGroup for Navy")
|
||||
logging.warning(f"{self.faction_name} has no ForceGroup for Navy")
|
||||
return
|
||||
self.generate_ground_object_from_group(unit_group, position)
|
||||
|
||||
@@ -217,11 +202,9 @@ class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
)
|
||||
return False
|
||||
|
||||
unit_group = self.faction.random_group_for_role_and_task(
|
||||
GroupRole.Naval, GroupTask.AircraftCarrier
|
||||
)
|
||||
unit_group = self.armed_forces.random_group_for_task(GroupTask.AIRCRAFT_CARRIER)
|
||||
if not unit_group:
|
||||
logging.error(f"{self.faction_name} has no UnitGroup for AircraftCarrier")
|
||||
logging.error(f"{self.faction_name} has no access to AircraftCarrier")
|
||||
return False
|
||||
self.generate_ground_object_from_group(
|
||||
unit_group,
|
||||
@@ -246,11 +229,11 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
)
|
||||
return False
|
||||
|
||||
unit_group = self.faction.random_group_for_role_and_task(
|
||||
GroupRole.Naval, GroupTask.HelicopterCarrier
|
||||
unit_group = self.armed_forces.random_group_for_task(
|
||||
GroupTask.HELICOPTER_CARRIER
|
||||
)
|
||||
if not unit_group:
|
||||
logging.error(f"{self.faction_name} has no UnitGroup for HelicopterCarrier")
|
||||
logging.error(f"{self.faction_name} has no access to HelicopterCarrier")
|
||||
return False
|
||||
self.generate_ground_object_from_group(
|
||||
unit_group,
|
||||
@@ -293,11 +276,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
|
||||
def generate_armor_groups(self) -> None:
|
||||
for position in self.control_point.preset_locations.armor_groups:
|
||||
unit_group = self.faction.random_group_for_role_and_tasks(
|
||||
GroupRole.GroundForce, ROLE_TASKINGS[GroupRole.GroundForce]
|
||||
)
|
||||
unit_group = self.armed_forces.random_group_for_task(GroupTask.BASE_DEFENSE)
|
||||
if not unit_group:
|
||||
logging.error(f"{self.faction_name} has no templates for Armor Groups")
|
||||
logging.error(f"{self.faction_name} has no ForceGroup for Armor")
|
||||
return
|
||||
self.generate_ground_object_from_group(unit_group, position)
|
||||
|
||||
@@ -318,11 +299,11 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
|
||||
def generate_ewrs(self) -> None:
|
||||
for position in self.control_point.preset_locations.ewrs:
|
||||
unit_group = self.faction.random_group_for_role_and_task(
|
||||
GroupRole.AntiAir, GroupTask.EWR
|
||||
unit_group = self.armed_forces.random_group_for_task(
|
||||
GroupTask.EARLY_WARNING_RADAR
|
||||
)
|
||||
if not unit_group:
|
||||
logging.error(f"{self.faction_name} has no UnitGroup for EWR")
|
||||
logging.error(f"{self.faction_name} has no ForceGroup for EWR")
|
||||
return
|
||||
self.generate_ground_object_from_group(unit_group, position)
|
||||
|
||||
@@ -331,31 +312,27 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
group_task: GroupTask,
|
||||
position: PointWithHeading,
|
||||
) -> None:
|
||||
unit_group = self.faction.random_group_for_role_and_task(
|
||||
GroupRole.Building, group_task
|
||||
)
|
||||
# GroupTask is the type of the building to be generated
|
||||
unit_group = self.armed_forces.random_group_for_task(group_task)
|
||||
if not unit_group:
|
||||
logging.error(
|
||||
f"{self.faction_name} has no access to Building ({group_task.value})"
|
||||
raise RuntimeError(
|
||||
f"{self.faction_name} has no access to Building {group_task.description}"
|
||||
)
|
||||
return
|
||||
self.generate_ground_object_from_group(unit_group, position)
|
||||
|
||||
def generate_ammunition_depots(self) -> None:
|
||||
for position in self.control_point.preset_locations.ammunition_depots:
|
||||
self.generate_building_at(GroupTask.Ammo, position)
|
||||
self.generate_building_at(GroupTask.AMMO, position)
|
||||
|
||||
def generate_factories(self) -> None:
|
||||
for position in self.control_point.preset_locations.factories:
|
||||
self.generate_building_at(GroupTask.Factory, position)
|
||||
self.generate_building_at(GroupTask.FACTORY, position)
|
||||
|
||||
def generate_aa_at(
|
||||
self, position: PointWithHeading, tasks: list[GroupTask]
|
||||
) -> None:
|
||||
for task in tasks:
|
||||
unit_group = self.faction.random_group_for_role_and_task(
|
||||
GroupRole.AntiAir, task
|
||||
)
|
||||
unit_group = self.armed_forces.random_group_for_task(task)
|
||||
if unit_group:
|
||||
# Only take next (smaller) aa_range when no template available for the
|
||||
# most requested range. Otherwise break the loop and continue
|
||||
@@ -363,7 +340,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
return
|
||||
|
||||
logging.error(
|
||||
f"{self.faction_name} has no access to SAM Templates ({', '.join([task.value for task in tasks])})"
|
||||
f"{self.faction_name} has no access to SAM {', '.join([task.description for task in tasks])}"
|
||||
)
|
||||
|
||||
def generate_scenery_sites(self) -> None:
|
||||
@@ -380,21 +357,20 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
Heading.from_degrees(0),
|
||||
self.control_point,
|
||||
)
|
||||
ground_group = GroundGroup(
|
||||
ground_group = TheaterGroup(
|
||||
self.game.next_group_id(),
|
||||
scenery.zone_def.name,
|
||||
PointWithHeading.from_point(scenery.position, Heading.from_degrees(0)),
|
||||
[],
|
||||
g,
|
||||
)
|
||||
ground_group.static_group = True
|
||||
g.groups.append(ground_group)
|
||||
# Each nested trigger zone is a target/building/unit for an objective.
|
||||
for zone in scenery.zones:
|
||||
scenery_unit = SceneryGroundUnit(
|
||||
scenery_unit = SceneryUnit(
|
||||
zone.id,
|
||||
zone.name,
|
||||
"",
|
||||
dcs.statics.Fortification.White_Flag,
|
||||
PointWithHeading.from_point(zone.position, Heading.from_degrees(0)),
|
||||
g,
|
||||
)
|
||||
@@ -405,31 +381,27 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
|
||||
def generate_missile_sites(self) -> None:
|
||||
for position in self.control_point.preset_locations.missile_sites:
|
||||
unit_group = self.faction.random_group_for_role_and_task(
|
||||
GroupRole.Defenses, GroupTask.Missile
|
||||
)
|
||||
unit_group = self.armed_forces.random_group_for_task(GroupTask.MISSILE)
|
||||
if not unit_group:
|
||||
logging.error(f"{self.faction_name} has no UnitGroup for Missile")
|
||||
logging.warning(f"{self.faction_name} has no ForceGroup for Missile")
|
||||
return
|
||||
self.generate_ground_object_from_group(unit_group, position)
|
||||
|
||||
def generate_coastal_sites(self) -> None:
|
||||
for position in self.control_point.preset_locations.coastal_defenses:
|
||||
unit_group = self.faction.random_group_for_role_and_task(
|
||||
GroupRole.Defenses, GroupTask.Coastal
|
||||
)
|
||||
unit_group = self.armed_forces.random_group_for_task(GroupTask.COASTAL)
|
||||
if not unit_group:
|
||||
logging.error(f"{self.faction_name} has no UnitGroup for Coastal")
|
||||
logging.warning(f"{self.faction_name} has no ForceGroup for Coastal")
|
||||
return
|
||||
self.generate_ground_object_from_group(unit_group, position)
|
||||
|
||||
def generate_strike_targets(self) -> None:
|
||||
for position in self.control_point.preset_locations.strike_locations:
|
||||
self.generate_building_at(GroupTask.StrikeTarget, position)
|
||||
self.generate_building_at(GroupTask.STRIKE_TARGET, position)
|
||||
|
||||
def generate_offshore_strike_targets(self) -> None:
|
||||
for position in self.control_point.preset_locations.offshore_strike_locations:
|
||||
self.generate_building_at(GroupTask.Oil, position)
|
||||
self.generate_building_at(GroupTask.OIL, position)
|
||||
|
||||
|
||||
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
||||
|
||||
@@ -3,28 +3,20 @@ from __future__ import annotations
|
||||
import itertools
|
||||
import logging
|
||||
from abc import ABC
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Iterator, List, TYPE_CHECKING, Union, Optional, Any
|
||||
from typing import Iterator, List, TYPE_CHECKING
|
||||
|
||||
from dcs.unittype import VehicleType, ShipType
|
||||
from dcs.unittype import VehicleType
|
||||
from dcs.vehicles import vehicle_map
|
||||
from dcs.ships import ship_map
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.triggers import TriggerZone
|
||||
|
||||
|
||||
from game.dcs.helpers import unit_type_from_name
|
||||
from ..data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS
|
||||
from ..dcs.groundunittype import GroundUnitType
|
||||
from ..dcs.shipunittype import ShipUnitType
|
||||
from ..dcs.unittype import UnitType
|
||||
from ..point_with_heading import PointWithHeading
|
||||
from ..utils import Distance, Heading, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.templates import UnitTemplate, GroupTemplate
|
||||
from .theatergroup import TheaterUnit, TheaterGroup
|
||||
from .controlpoint import ControlPoint
|
||||
from ..ato.flighttype import FlightType
|
||||
|
||||
@@ -53,126 +45,6 @@ NAME_BY_CATEGORY = {
|
||||
}
|
||||
|
||||
|
||||
class SkynetRole(Enum):
|
||||
#: A radar SAM that should be controlled by Skynet.
|
||||
Sam = "Sam"
|
||||
|
||||
#: A radar SAM that should be controlled and used as an EWR by Skynet.
|
||||
SamAsEwr = "SamAsEwr"
|
||||
|
||||
#: An air defense unit that should be used as point defense by Skynet.
|
||||
PointDefense = "PD"
|
||||
|
||||
#: All other types of groups that might be present in a SAM TGO. This includes
|
||||
#: SHORADS, AAA, supply trucks, etc. Anything that shouldn't be controlled by Skynet
|
||||
#: should use this role.
|
||||
NoSkynetBehavior = "NoSkynetBehavior"
|
||||
|
||||
|
||||
class AirDefenseRange(Enum):
|
||||
AAA = ("AAA", SkynetRole.NoSkynetBehavior)
|
||||
Short = ("short", SkynetRole.NoSkynetBehavior)
|
||||
Medium = ("medium", SkynetRole.Sam)
|
||||
Long = ("long", SkynetRole.SamAsEwr)
|
||||
|
||||
def __init__(self, description: str, default_role: SkynetRole) -> None:
|
||||
self.range_name = description
|
||||
self.default_role = default_role
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroundUnit:
|
||||
# Units can be everything.. Static, Vehicle, Ship.
|
||||
id: int
|
||||
name: str
|
||||
type: str # dcs.UnitType as string
|
||||
position: PointWithHeading
|
||||
ground_object: TheaterGroundObject
|
||||
alive: bool = True
|
||||
_unit_type: Optional[UnitType[Any]] = None
|
||||
|
||||
@staticmethod
|
||||
def from_template(
|
||||
id: int, unit_type: str, t: UnitTemplate, go: TheaterGroundObject
|
||||
) -> GroundUnit:
|
||||
return GroundUnit(
|
||||
id,
|
||||
t.name,
|
||||
unit_type,
|
||||
PointWithHeading.from_point(t.position, Heading.from_degrees(t.heading)),
|
||||
go,
|
||||
)
|
||||
|
||||
@property
|
||||
def unit_type(self) -> Optional[UnitType[Any]]:
|
||||
if not self._unit_type:
|
||||
try:
|
||||
unit_type: Optional[UnitType[Any]] = None
|
||||
dcs_type = db.unit_type_from_name(self.type)
|
||||
if dcs_type and issubclass(dcs_type, VehicleType):
|
||||
unit_type = next(GroundUnitType.for_dcs_type(dcs_type))
|
||||
elif dcs_type and issubclass(dcs_type, ShipType):
|
||||
unit_type = next(ShipUnitType.for_dcs_type(dcs_type))
|
||||
self._unit_type = unit_type
|
||||
except StopIteration:
|
||||
logging.error(f"No UnitType for {self.type}")
|
||||
pass
|
||||
return self._unit_type
|
||||
|
||||
def kill(self) -> None:
|
||||
self.alive = False
|
||||
|
||||
@property
|
||||
def unit_name(self) -> str:
|
||||
return f"{str(self.id).zfill(4)} | {self.name}"
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
dead_label = " [DEAD]" if not self.alive else ""
|
||||
unit_label = self.unit_type or self.type or self.name
|
||||
return f"{str(self.id).zfill(4)} | {unit_label}{dead_label}"
|
||||
|
||||
|
||||
class SceneryGroundUnit(GroundUnit):
|
||||
"""Special GroundUnit for handling scenery ground objects"""
|
||||
|
||||
zone: TriggerZone
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroundGroup:
|
||||
id: int
|
||||
name: str
|
||||
position: PointWithHeading
|
||||
units: list[GroundUnit]
|
||||
ground_object: TheaterGroundObject
|
||||
static_group: bool = False
|
||||
|
||||
@staticmethod
|
||||
def from_template(
|
||||
id: int,
|
||||
g: GroupTemplate,
|
||||
go: TheaterGroundObject,
|
||||
) -> GroundGroup:
|
||||
tgo_group = GroundGroup(
|
||||
id,
|
||||
g.name,
|
||||
PointWithHeading.from_point(go.position, go.heading),
|
||||
g.generate_units(go),
|
||||
go,
|
||||
)
|
||||
tgo_group.static_group = g.static
|
||||
return tgo_group
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
return f"{str(self.id).zfill(4)} | {self.name}"
|
||||
|
||||
@property
|
||||
def alive_units(self) -> int:
|
||||
return sum([unit.alive for unit in self.units])
|
||||
|
||||
|
||||
class TheaterGroundObject(MissionTarget):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -188,27 +60,28 @@ class TheaterGroundObject(MissionTarget):
|
||||
self.heading = heading
|
||||
self.control_point = control_point
|
||||
self.sea_object = sea_object
|
||||
self.groups: List[GroundGroup] = []
|
||||
self.groups: List[TheaterGroup] = []
|
||||
|
||||
@property
|
||||
def is_dead(self) -> bool:
|
||||
return self.alive_unit_count == 0
|
||||
|
||||
@property
|
||||
def units(self) -> Iterator[GroundUnit]:
|
||||
def units(self) -> Iterator[TheaterUnit]:
|
||||
"""
|
||||
:return: all the units at this location
|
||||
"""
|
||||
yield from itertools.chain.from_iterable([g.units for g in self.groups])
|
||||
|
||||
@property
|
||||
def statics(self) -> Iterator[GroundUnit]:
|
||||
def statics(self) -> Iterator[TheaterUnit]:
|
||||
for group in self.groups:
|
||||
if group.static_group:
|
||||
yield from group.units
|
||||
for unit in group.units:
|
||||
if unit.is_static:
|
||||
yield unit
|
||||
|
||||
@property
|
||||
def dead_units(self) -> list[GroundUnit]:
|
||||
def dead_units(self) -> list[TheaterUnit]:
|
||||
"""
|
||||
:return: all the dead units at this location
|
||||
"""
|
||||
@@ -253,6 +126,10 @@ class TheaterGroundObject(MissionTarget):
|
||||
]
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def unit_count(self) -> int:
|
||||
return sum([g.unit_count for g in self.groups])
|
||||
|
||||
@property
|
||||
def alive_unit_count(self) -> int:
|
||||
return sum([g.alive_units for g in self.groups])
|
||||
@@ -269,20 +146,15 @@ class TheaterGroundObject(MissionTarget):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _max_range_of_type(self, group: GroundGroup, range_type: str) -> Distance:
|
||||
def _max_range_of_type(self, group: TheaterGroup, range_type: str) -> Distance:
|
||||
if not self.might_have_aa:
|
||||
return meters(0)
|
||||
|
||||
max_range = meters(0)
|
||||
for u in group.units:
|
||||
unit = unit_type_from_name(u.type)
|
||||
if unit is None:
|
||||
logging.error(f"Unknown unit type {u.type}")
|
||||
continue
|
||||
|
||||
# Some units in pydcs have detection_range/threat_range defined,
|
||||
# but explicitly set to None.
|
||||
unit_range = getattr(unit, range_type, None)
|
||||
unit_range = getattr(u.type, range_type, None)
|
||||
if unit_range is not None:
|
||||
max_range = max(max_range, meters(unit_range))
|
||||
return max_range
|
||||
@@ -290,7 +162,7 @@ class TheaterGroundObject(MissionTarget):
|
||||
def max_detection_range(self) -> Distance:
|
||||
return max(self.detection_range(g) for g in self.groups)
|
||||
|
||||
def detection_range(self, group: GroundGroup) -> Distance:
|
||||
def detection_range(self, group: TheaterGroup) -> Distance:
|
||||
return self._max_range_of_type(group, "detection_range")
|
||||
|
||||
def max_threat_range(self) -> Distance:
|
||||
@@ -298,7 +170,7 @@ class TheaterGroundObject(MissionTarget):
|
||||
max(self.threat_range(g) for g in self.groups) if self.groups else meters(0)
|
||||
)
|
||||
|
||||
def threat_range(self, group: GroundGroup, radar_only: bool = False) -> Distance:
|
||||
def threat_range(self, group: TheaterGroup, radar_only: bool = False) -> Distance:
|
||||
return self._max_range_of_type(group, "threat_range")
|
||||
|
||||
@property
|
||||
@@ -315,7 +187,7 @@ class TheaterGroundObject(MissionTarget):
|
||||
return False
|
||||
|
||||
@property
|
||||
def strike_targets(self) -> list[GroundUnit]:
|
||||
def strike_targets(self) -> list[TheaterUnit]:
|
||||
return [unit for unit in self.units if unit.alive]
|
||||
|
||||
@property
|
||||
@@ -543,13 +415,15 @@ class SamGroundObject(IadsGroundObject):
|
||||
def might_have_aa(self) -> bool:
|
||||
return True
|
||||
|
||||
def threat_range(self, group: GroundGroup, radar_only: bool = False) -> Distance:
|
||||
def threat_range(self, group: TheaterGroup, radar_only: bool = False) -> Distance:
|
||||
max_non_radar = meters(0)
|
||||
live_trs = set()
|
||||
max_telar_range = meters(0)
|
||||
launchers = set()
|
||||
for unit in group.units:
|
||||
unit_type = vehicle_map[unit.type]
|
||||
if not unit.alive or not issubclass(unit.type, VehicleType):
|
||||
continue
|
||||
unit_type = unit.type
|
||||
if unit_type in TRACK_RADARS:
|
||||
live_trs.add(unit_type)
|
||||
elif unit_type in TELARS:
|
||||
|
||||
167
game/theater/theatergroup.py
Normal file
167
game/theater/theatergroup.py
Normal file
@@ -0,0 +1,167 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Any, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.triggers import TriggerZone
|
||||
from dcs.unittype import VehicleType, ShipType, StaticType
|
||||
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.dcs.shipunittype import ShipUnitType
|
||||
from game.dcs.unittype import UnitType
|
||||
from dcs.unittype import UnitType as DcsUnitType
|
||||
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from game.utils import Heading
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.layout.layout import LayoutUnit, GroupLayout
|
||||
from game.theater import TheaterGroundObject
|
||||
|
||||
|
||||
@dataclass
|
||||
class TheaterUnit:
|
||||
"""Representation of a single Unit in the Game"""
|
||||
|
||||
# Every Unit has a unique ID generated from the game
|
||||
id: int
|
||||
# The name of the Unit. Not required to be unique
|
||||
name: str
|
||||
# DCS UniType of the unit
|
||||
type: Type[DcsUnitType]
|
||||
# Position and orientation of the Unit
|
||||
position: PointWithHeading
|
||||
# The parent ground object
|
||||
ground_object: TheaterGroundObject
|
||||
# State of the unit, dead or alive
|
||||
alive: bool = True
|
||||
|
||||
@staticmethod
|
||||
def from_template(
|
||||
id: int, dcs_type: Type[DcsUnitType], t: LayoutUnit, go: TheaterGroundObject
|
||||
) -> TheaterUnit:
|
||||
return TheaterUnit(
|
||||
id,
|
||||
t.name,
|
||||
dcs_type,
|
||||
PointWithHeading.from_point(t.position, Heading.from_degrees(t.heading)),
|
||||
go,
|
||||
)
|
||||
|
||||
@property
|
||||
def unit_type(self) -> Optional[UnitType[Any]]:
|
||||
if issubclass(self.type, VehicleType):
|
||||
return next(GroundUnitType.for_dcs_type(self.type))
|
||||
elif issubclass(self.type, ShipType):
|
||||
return next(ShipUnitType.for_dcs_type(self.type))
|
||||
# None for not available StaticTypes
|
||||
return None
|
||||
|
||||
def kill(self) -> None:
|
||||
self.alive = False
|
||||
|
||||
@property
|
||||
def unit_name(self) -> str:
|
||||
return f"{str(self.id).zfill(4)} | {self.name}"
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
dead_label = " [DEAD]" if not self.alive else ""
|
||||
unit_label = self.unit_type or self.type.name or self.name
|
||||
return f"{str(self.id).zfill(4)} | {unit_label}{dead_label}"
|
||||
|
||||
@property
|
||||
def short_name(self) -> str:
|
||||
dead_label = " [DEAD]" if not self.alive else ""
|
||||
return f"<b>{self.type.id[0:18]}</b> {dead_label}"
|
||||
|
||||
@property
|
||||
def is_static(self) -> bool:
|
||||
return issubclass(self.type, StaticType)
|
||||
|
||||
@property
|
||||
def is_vehicle(self) -> bool:
|
||||
return issubclass(self.type, VehicleType)
|
||||
|
||||
@property
|
||||
def is_ship(self) -> bool:
|
||||
return issubclass(self.type, ShipType)
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
return self.type.id
|
||||
|
||||
@property
|
||||
def repairable(self) -> bool:
|
||||
# Only let units with UnitType be repairable as we just have prices for them
|
||||
return self.unit_type is not None
|
||||
|
||||
|
||||
class SceneryUnit(TheaterUnit):
|
||||
"""Special TheaterUnit for handling scenery ground objects"""
|
||||
|
||||
# Scenery Objects are identified by a special trigger zone
|
||||
zone: TriggerZone
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
dead_label = " [DEAD]" if not self.alive else ""
|
||||
return f"{str(self.id).zfill(4)} | {self.name}{dead_label}"
|
||||
|
||||
@property
|
||||
def short_name(self) -> str:
|
||||
dead_label = " [DEAD]" if not self.alive else ""
|
||||
return f"<b>{self.name[0:18]}</b> {dead_label}"
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
return "missing"
|
||||
|
||||
@property
|
||||
def repairable(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
@dataclass
|
||||
class TheaterGroup:
|
||||
"""Logical group for multiple TheaterUnits at a specific position"""
|
||||
|
||||
# Every Theater Group has a unique ID generated from the game
|
||||
id: int # Unique ID
|
||||
# The name of the Group. Not required to be unique
|
||||
name: str
|
||||
# Position and orientation of the Group
|
||||
position: PointWithHeading
|
||||
# All TheaterUnits within the group
|
||||
units: list[TheaterUnit]
|
||||
# The parent ground object
|
||||
ground_object: TheaterGroundObject
|
||||
|
||||
@staticmethod
|
||||
def from_template(
|
||||
id: int,
|
||||
g: GroupLayout,
|
||||
go: TheaterGroundObject,
|
||||
unit_type: Type[DcsUnitType],
|
||||
unit_count: int,
|
||||
) -> TheaterGroup:
|
||||
return TheaterGroup(
|
||||
id,
|
||||
g.name,
|
||||
PointWithHeading.from_point(go.position, go.heading),
|
||||
g.generate_units(go, unit_type, unit_count),
|
||||
go,
|
||||
)
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
return f"{str(self.id).zfill(4)} | {self.name}"
|
||||
|
||||
@property
|
||||
def unit_count(self) -> int:
|
||||
return len(self.units)
|
||||
|
||||
@property
|
||||
def alive_units(self) -> int:
|
||||
return sum([unit.alive for unit in self.units])
|
||||
@@ -12,9 +12,9 @@ from dcs.unitgroup import FlyingGroup, VehicleGroup, ShipGroup
|
||||
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.squadrons import Pilot
|
||||
from game.theater import Airfield, ControlPoint, GroundUnit
|
||||
from game.theater import Airfield, ControlPoint, TheaterUnit
|
||||
from game.ato.flight import Flight
|
||||
from game.theater.theatergroundobject import SceneryGroundUnit
|
||||
from game.theater.theatergroup import SceneryUnit
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.transfers import CargoShip, Convoy, TransferOrder
|
||||
@@ -33,14 +33,14 @@ class FrontLineUnit:
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GroundObjectMapping:
|
||||
ground_unit: GroundUnit
|
||||
class TheaterUnitMapping:
|
||||
theater_unit: TheaterUnit
|
||||
dcs_unit: Unit
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SceneryObjectMapping:
|
||||
ground_unit: GroundUnit
|
||||
ground_unit: TheaterUnit
|
||||
trigger_zone: TriggerZone
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ class UnitMap:
|
||||
self.aircraft: Dict[str, FlyingUnit] = {}
|
||||
self.airfields: Dict[str, Airfield] = {}
|
||||
self.front_line_units: Dict[str, FrontLineUnit] = {}
|
||||
self.ground_objects: Dict[str, GroundObjectMapping] = {}
|
||||
self.theater_objects: Dict[str, TheaterUnitMapping] = {}
|
||||
self.scenery_objects: Dict[str, SceneryObjectMapping] = {}
|
||||
self.convoys: Dict[str, ConvoyUnit] = {}
|
||||
self.cargo_ships: Dict[str, CargoShip] = {}
|
||||
@@ -103,18 +103,18 @@ class UnitMap:
|
||||
def front_line_unit(self, name: str) -> Optional[FrontLineUnit]:
|
||||
return self.front_line_units.get(name, None)
|
||||
|
||||
def add_ground_object_mapping(
|
||||
self, ground_unit: GroundUnit, dcs_unit: Unit
|
||||
def add_theater_unit_mapping(
|
||||
self, theater_unit: TheaterUnit, dcs_unit: Unit
|
||||
) -> None:
|
||||
# Deaths for units at TGOs are recorded in the corresponding GroundUnit within
|
||||
# the GroundGroup, so we have to match the dcs unit with the liberation unit
|
||||
name = str(dcs_unit.name)
|
||||
if name in self.ground_objects:
|
||||
if name in self.theater_objects:
|
||||
raise RuntimeError(f"Duplicate TGO unit: {name}")
|
||||
self.ground_objects[name] = GroundObjectMapping(ground_unit, dcs_unit)
|
||||
self.theater_objects[name] = TheaterUnitMapping(theater_unit, dcs_unit)
|
||||
|
||||
def ground_object(self, name: str) -> Optional[GroundObjectMapping]:
|
||||
return self.ground_objects.get(name, None)
|
||||
def theater_units(self, name: str) -> Optional[TheaterUnitMapping]:
|
||||
return self.theater_objects.get(name, None)
|
||||
|
||||
def add_convoy_units(self, group: VehicleGroup, convoy: Convoy) -> None:
|
||||
for unit, unit_type in zip(group.units, convoy.iter_units()):
|
||||
@@ -170,9 +170,7 @@ class UnitMap:
|
||||
def airlift_unit(self, name: str) -> Optional[AirliftUnits]:
|
||||
return self.airlifts.get(name, None)
|
||||
|
||||
def add_scenery(
|
||||
self, scenery_unit: SceneryGroundUnit, trigger_zone: TriggerZone
|
||||
) -> None:
|
||||
def add_scenery(self, scenery_unit: SceneryUnit, trigger_zone: TriggerZone) -> None:
|
||||
name = str(trigger_zone.name)
|
||||
if name in self.scenery_objects:
|
||||
raise RuntimeError(f"Duplicate scenery object {name} (TriggerZone)")
|
||||
|
||||
Reference in New Issue
Block a user