Implement Template System for ground objects

- Factored out the current generators to use a better approach with Templates build from the dcs mission editor.
- This information is extended with a template-mapping in a json file which allows to logically group together multiple dcs groups and even statics to one template
- The combination of mapping and miz will be serialized to a template.json which is only used for loading.
- Factions now load templates during initialization and hold all the templates they can really use. This is based around the faction file.
- Implemented a template randomizer which allows to add some randomization to templates
- Each Template Group can have 1 randomizer which randomizes unit_type and size based on the mapping definition. Larger groups need to be devided in more fine detailed groups as we can now handle them better due to the change from dcs group types to our own classes.
- Rewritten the ArmorGroup, Naval and EWR template handling

Rework GroundObjectBuyMenu to support templates
This commit is contained in:
RndName 2022-01-19 13:06:19 +01:00
parent d154069877
commit 5febcdd4e4
11 changed files with 1884 additions and 591 deletions

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import copy
import itertools
import logging
from dataclasses import dataclass, field
@ -24,6 +25,7 @@ from game.data.doctrine import (
from game.data.groundunitclass import GroundUnitClass
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from gen.templates import GroundObjectTemplates, TemplateCategory
if TYPE_CHECKING:
from game.theater.start_generator import ModSettings
@ -72,7 +74,7 @@ class Faction:
air_defenses: List[str] = field(default_factory=list)
# Possible EWR generators for this faction.
ewrs: List[str] = field(default_factory=list)
ewrs: List[GroundUnitType] = field(default_factory=list)
# Possible Missile site generators for this faction
missiles: List[str] = field(default_factory=list)
@ -137,12 +139,68 @@ class Faction:
#: both will use it.
unrestricted_satnav: bool = False
def has_access_to_unittype(self, unit_class: GroundUnitClass) -> bool:
# All possible templates which can be generated by the faction
templates: GroundObjectTemplates = field(default=GroundObjectTemplates())
def __getitem__(self, item: str) -> Any:
return getattr(self, item)
def has_access_to_unit_type(self, unit_type: str) -> bool:
# Supports all GroundUnit lists and AirDefenses
for unit in self.ground_units:
if unit_type == unit.dcs_id:
return True
return unit_type in self.air_defenses
def has_access_to_unit_class(self, unit_class: GroundUnitClass) -> bool:
for vehicle in itertools.chain(self.frontline_units, self.artillery_units):
if vehicle.unit_class is unit_class:
return True
return False
def load_templates(self, all_templates: GroundObjectTemplates) -> None:
# This loads all faction possible sam templates and the default ones
# For legacy reasons this allows to check for template names. This can be
# improved in the future to have more control about the possible Templates.
# For example it can be possible to define the unit_types and check if all
# requirements for the template are fulfilled.
for category, template in all_templates.templates:
if (
(
category == TemplateCategory.AirDefence
and (
# Check if faction has the template name or ALL required
# unit_types in the list air_defenses. For legacy reasons this
# allows both and also the EWR template
template.name in self.air_defenses
or all(
self.has_access_to_unit_type(required_unit)
for required_unit in template.required_units
)
or template.template_type == "EWR"
)
)
or template.name in self.navy_generators
or template.name in self.missiles
or template.name in self.coastal_defenses
or (
template.template_type
in self.building_set + ["fob", "ammo", "factory"]
)
or (template.template_type == "carrier" and self.aircraft_carrier)
or (template.template_type == "lha" and self.helicopter_carrier)
or category == TemplateCategory.Armor
):
# 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)
# Initialize all randomizers
for group_template in faction_template.groups:
if group_template.randomizer:
group_template.randomizer.init_randomization_for_faction(self)
self.templates.add_template(category, faction_template)
@classmethod
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
faction = Faction(locales=json.get("locales"))
@ -182,8 +240,7 @@ class Faction:
faction.logistics_units = [
GroundUnitType.named(n) for n in json.get("logistics_units", [])
]
faction.ewrs = json.get("ewrs", [])
faction.ewrs = [GroundUnitType.named(n) for n in json.get("ewrs", [])]
faction.air_defenses = json.get("air_defenses", [])
# Compatibility for older factions. All air defenses now belong to a
@ -245,6 +302,8 @@ class Faction:
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
# Templates
faction.templates = GroundObjectTemplates()
return faction
@property
@ -320,13 +379,13 @@ class Faction:
self.remove_vehicle("KORNET")
# high digit sams
if not mod_settings.high_digit_sams:
self.remove_air_defenses("SA10BGenerator")
self.remove_air_defenses("SA12Generator")
self.remove_air_defenses("SA20Generator")
self.remove_air_defenses("SA20BGenerator")
self.remove_air_defenses("SA23Generator")
self.remove_air_defenses("SA17Generator")
self.remove_air_defenses("KS19Generator")
self.remove_air_defenses("SA-10B/S-300PS Battery")
self.remove_air_defenses("SA-12/S-300V Battery")
self.remove_air_defenses("SA-20/S-300PMU-1 Battery")
self.remove_air_defenses("SA-20B/S-300PMU-2 Battery")
self.remove_air_defenses("SA-23/S-300VM Battery")
self.remove_air_defenses("SA-17 Grizzly Battery")
self.remove_air_defenses("KS-19 AAA Site")
return self
def remove_aircraft(self, name: str) -> None:

View File

@ -123,7 +123,7 @@ class GroundObjectGenerator:
if not u.alive:
continue
unit_type = static_type_from_name(u.type)
unit_type = unit_type_from_name(u.type)
if not unit_type:
raise RuntimeError(
f"Unit type {u.type} is not a valid dcs unit type"

View File

@ -158,7 +158,7 @@ class ProcurementAi:
worst_balanced: Optional[GroundUnitClass] = None
worst_fulfillment = math.inf
for unit_class in GroundUnitClass:
if not self.faction.has_access_to_unittype(unit_class):
if not self.faction.has_access_to_unit_class(unit_class):
continue
current_ratio = self.cost_ratio_of_ground_unit(cp, unit_class)

View File

@ -1,45 +1,25 @@
from __future__ import annotations
import logging
import pickle
import random
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Dict, Iterable, List, Set
from dcs.mapping import Point
from typing import List
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,
CarrierGroundObject,
EwrGroundObject,
FactoryGroundObject,
LhaGroundObject,
MissileSiteGroundObject,
SamGroundObject,
ShipGroundObject,
SceneryGroundObject,
VehicleGroupGroundObject,
CoastalSiteGroundObject,
SceneryGroundUnit,
GroundGroup,
)
from game.utils import Heading
from game.version import VERSION
from gen.coastal.coastal_group_generator import generate_coastal_group
from gen.defenses.armor_group_generator import generate_armor_group
from gen.fleet.ship_group_generator import (
generate_carrier_group,
generate_lha_group,
generate_ship_group,
)
from gen.missiles.missiles_group_generator import generate_missile_group
from gen.templates import GroundObjectTemplates, TemplateCategory, GroundObjectTemplate
from gen.naming import namegen
from gen.sam.airdefensegroupgenerator import AirDefenseRange
from gen.sam.ewr_group_generator import generate_ewr_group
from gen.sam.sam_group_generator import generate_anti_air_group
from . import (
ConflictTheater,
ControlPoint,
@ -51,8 +31,6 @@ from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
from ..profiling import logged_duration
from ..settings import Settings
GroundObjectTemplates = Dict[str, Dict[str, Any]]
@dataclass(frozen=True)
class GeneratorSettings:
@ -97,6 +75,9 @@ class GameGenerator:
self.settings = settings
self.generator_settings = generator_settings
with logged_duration(f"Initializing templates"):
self.load_templates()
def generate(self) -> Game:
with logged_duration("TGO population"):
# Reset name generator
@ -142,6 +123,13 @@ class GameGenerator:
for cp in to_remove:
self.theater.controlpoints.remove(cp)
def load_templates(self) -> None:
templates = GroundObjectTemplates.from_json(
"resources/templates/templates.json"
)
self.player.load_templates(templates)
self.enemy.load_templates(templates)
class ControlPointGroundObjectGenerator:
def __init__(
@ -170,6 +158,26 @@ class ControlPointGroundObjectGenerator:
return True
def generate_random_from_templates(
self, templates: list[GroundObjectTemplate], position: PointWithHeading
) -> None:
try:
template = random.choice(templates)
with logged_duration(
f"Ground Object generation from template {template.name}"
):
ground_object = template.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")
def generate_navy(self) -> None:
skip_player_navy = self.generator_settings.no_player_navy
if self.control_point.captured and skip_player_navy:
@ -179,21 +187,12 @@ class ControlPointGroundObjectGenerator:
if not self.control_point.captured and skip_enemy_navy:
return
for position in self.control_point.preset_locations.ships:
self.generate_ship_at(position)
def generate_ship_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = ShipGroundObject(
namegen.random_objective_name(), group_id, position, self.control_point
templates = list(
self.faction.templates.for_category(TemplateCategory.Naval, "ship")
)
group = generate_ship_group(self.game, g, self.faction_name)
g.groups = []
if group is not None:
g.groups.append(group)
self.control_point.connected_objectives.append(g)
for position in self.control_point.preset_locations.ships:
self.generate_random_from_templates(templates, position)
class NoOpGroundObjectGenerator(ControlPointGroundObjectGenerator):
@ -214,16 +213,16 @@ class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
)
return False
# Create ground object group
group_id = self.game.next_group_id()
g = CarrierGroundObject(
namegen.random_objective_name(), group_id, self.control_point
templates = list(
self.faction.templates.for_category(TemplateCategory.Naval, "carrier")
)
self.generate_random_from_templates(
templates,
PointWithHeading.from_point(
self.control_point.position, self.control_point.heading
),
)
group = generate_carrier_group(self.faction_name, self.game, g)
g.groups = []
if group is not None:
g.groups.append(group)
self.control_point.connected_objectives.append(g)
self.control_point.name = random.choice(carrier_names)
return True
@ -241,16 +240,16 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
)
return False
# Create ground object group
group_id = self.game.next_group_id()
g = LhaGroundObject(
namegen.random_objective_name(), group_id, self.control_point
templates = list(
self.faction.templates.for_category(TemplateCategory.Naval, "lha")
)
self.generate_random_from_templates(
templates,
PointWithHeading.from_point(
self.control_point.position, self.control_point.heading
),
)
group = generate_lha_group(self.faction_name, self.game, g)
g.groups = []
if group is not None:
g.groups.append(group)
self.control_point.connected_objectives.append(g)
self.control_point.name = random.choice(lha_names)
return True
@ -261,10 +260,8 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
game: Game,
generator_settings: GeneratorSettings,
control_point: ControlPoint,
templates: GroundObjectTemplates,
) -> None:
super().__init__(game, generator_settings, control_point)
self.templates = templates
def generate(self) -> bool:
if not super().generate():
@ -291,156 +288,102 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.generate_coastal_sites()
def generate_armor_groups(self) -> None:
for position in self.control_point.preset_locations.armor_groups:
self.generate_armor_at(position)
def generate_armor_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = VehicleGroupGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
)
group = generate_armor_group(self.faction_name, self.game, g)
if group is None:
logging.error(
"Could not generate armor group for %s at %s",
g.name,
self.control_point,
)
templates = list(self.faction.templates.for_category(TemplateCategory.Armor))
if not templates:
logging.error(f"{self.faction_name} has no access to Armor templates")
return
g.groups = [group]
self.control_point.connected_objectives.append(g)
for position in self.control_point.preset_locations.armor_groups:
self.generate_random_from_templates(templates, position)
def generate_aa(self) -> None:
presets = self.control_point.preset_locations
for position in presets.long_range_sams:
self.generate_aa_at(
position,
ranges=[
{AirDefenseRange.Long},
{AirDefenseRange.Medium},
{AirDefenseRange.Short},
{AirDefenseRange.AAA},
[
AirDefenseRange.Long,
AirDefenseRange.Medium,
AirDefenseRange.Short,
AirDefenseRange.AAA,
],
)
for position in presets.medium_range_sams:
self.generate_aa_at(
position,
ranges=[
{AirDefenseRange.Medium},
{AirDefenseRange.Short},
{AirDefenseRange.AAA},
[
AirDefenseRange.Medium,
AirDefenseRange.Short,
AirDefenseRange.AAA,
],
)
for position in presets.short_range_sams:
self.generate_aa_at(
position,
ranges=[{AirDefenseRange.Short}, {AirDefenseRange.AAA}],
[AirDefenseRange.Short, AirDefenseRange.AAA],
)
for position in presets.aaa:
self.generate_aa_at(
position,
ranges=[{AirDefenseRange.AAA}],
[AirDefenseRange.AAA],
)
def generate_ewrs(self) -> None:
presets = self.control_point.preset_locations
for position in presets.ewrs:
self.generate_ewr_at(position)
templates = list(
self.faction.templates.for_category(TemplateCategory.AirDefence, "EWR")
)
if not templates:
logging.error(f"{self.faction_name} has no access to EWR templates")
return
def generate_strike_target_at(self, category: str, position: Point) -> None:
for position in self.control_point.preset_locations.ewrs:
self.generate_random_from_templates(templates, position)
obj_name = namegen.random_objective_name()
template = random.choice(list(self.templates[category].values()))
object_id = 0
group_id = self.game.next_group_id()
# TODO: Create only one TGO per objective, each with multiple units.
for unit in template:
object_id += 1
template_point = Point(unit["offset"].x, unit["offset"].y)
g = BuildingGroundObject(
obj_name,
category,
group_id,
object_id,
position + template_point,
Heading.from_degrees(unit["heading"]),
self.control_point,
unit["type"],
def generate_building_at(
self,
template_category: TemplateCategory,
building_category: str,
position: PointWithHeading,
) -> None:
templates = list(
self.faction.templates.for_category(template_category, building_category)
)
if templates:
self.generate_random_from_templates(templates, position)
else:
logging.error(
f"{self.faction_name} has no access to Building type {building_category}"
)
self.control_point.connected_objectives.append(g)
def generate_ammunition_depots(self) -> None:
for position in self.control_point.preset_locations.ammunition_depots:
self.generate_strike_target_at(category="ammo", position=position)
self.generate_building_at(TemplateCategory.Building, "ammo", position)
def generate_factories(self) -> None:
for position in self.control_point.preset_locations.factories:
self.generate_factory_at(position)
def generate_factory_at(self, point: PointWithHeading) -> None:
obj_name = namegen.random_objective_name()
group_id = self.game.next_group_id()
g = FactoryGroundObject(
obj_name,
group_id,
point,
point.heading,
self.control_point,
)
self.control_point.connected_objectives.append(g)
self.generate_building_at(TemplateCategory.Building, "factory", position)
def generate_aa_at(
self, position: Point, ranges: Iterable[Set[AirDefenseRange]]
self, position: PointWithHeading, ranges: list[AirDefenseRange]
) -> None:
group_id = self.game.next_group_id()
g = SamGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
)
groups = generate_anti_air_group(self.game, g, self.faction, ranges)
if not groups:
templates = []
for aa_range in ranges:
for template in self.faction.templates.for_category(
TemplateCategory.AirDefence, aa_range.name
):
templates.append(template)
if len(templates) > 0:
# Only take next (smaller) aa_range when no template available for the
# most requested range. Otherwise break the loop and continue
break
if templates:
self.generate_random_from_templates(templates, position)
else:
logging.error(
"Could not generate air defense group for %s at %s",
g.name,
self.control_point,
f"{self.faction_name} has no access to SAM Templates ({', '.join([range.name for range in ranges])})"
)
return
g.groups = groups
self.control_point.connected_objectives.append(g)
def generate_ewr_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = EwrGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
)
group = generate_ewr_group(self.game, g, self.faction)
if group is None:
logging.error(
"Could not generate ewr group for %s at %s",
g.name,
self.control_point,
)
return
g.groups = [group]
self.control_point.connected_objectives.append(g)
def generate_scenery_sites(self) -> None:
presets = self.control_point.preset_locations
@ -448,143 +391,93 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.generate_tgo_for_scenery(scenery_group)
def generate_tgo_for_scenery(self, scenery: SceneryGroup) -> None:
obj_name = namegen.random_objective_name()
category = scenery.category
group_id = self.game.next_group_id()
object_id = 0
# Special Handling for scenery Objects based on trigger zones
g = BuildingGroundObject(
namegen.random_objective_name(),
scenery.category,
scenery.position,
Heading.from_degrees(0),
self.control_point,
)
ground_group = GroundGroup(
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:
object_id += 1
local_position = zone.position
local_dcs_identifier = zone.name
g = SceneryGroundObject(
obj_name,
category,
group_id,
object_id,
local_position,
self.control_point,
local_dcs_identifier,
zone,
scenery_unit = SceneryGroundUnit(
zone.id,
zone.name,
"",
PointWithHeading.from_point(zone.position, Heading.from_degrees(0)),
g,
)
scenery_unit.zone = zone
ground_group.units.append(scenery_unit)
self.control_point.connected_objectives.append(g)
return
self.control_point.connected_objectives.append(g)
def generate_missile_sites(self) -> None:
templates = list(self.faction.templates.for_category(TemplateCategory.Missile))
if not templates:
logging.error(f"{self.faction_name} has no access to Missile templates")
return
for position in self.control_point.preset_locations.missile_sites:
self.generate_missile_site_at(position)
def generate_missile_site_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = MissileSiteGroundObject(
namegen.random_objective_name(), group_id, position, self.control_point
)
group = generate_missile_group(self.game, g, self.faction_name)
g.groups = []
if group is not None:
g.groups.append(group)
self.control_point.connected_objectives.append(g)
return
self.generate_random_from_templates(templates, position)
def generate_coastal_sites(self) -> None:
templates = list(self.faction.templates.for_category(TemplateCategory.Coastal))
if not templates:
logging.error(f"{self.faction_name} has no access to Coastal templates")
return
for position in self.control_point.preset_locations.coastal_defenses:
self.generate_coastal_site_at(position)
def generate_coastal_site_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = CoastalSiteGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
position.heading,
)
group = generate_coastal_group(self.game, g, self.faction_name)
g.groups = []
if group is not None:
g.groups.append(group)
self.control_point.connected_objectives.append(g)
return
self.generate_random_from_templates(templates, position)
def generate_strike_targets(self) -> None:
building_set = list(set(self.faction.building_set) - {"oil"})
if not building_set:
logging.error("Faction has no buildings defined")
logging.error(f"{self.faction_name} has no buildings defined")
return
for position in self.control_point.preset_locations.strike_locations:
category = random.choice(building_set)
self.generate_strike_target_at(category, position)
self.generate_building_at(TemplateCategory.Building, category, position)
def generate_offshore_strike_targets(self) -> None:
if "oil" not in self.faction.building_set:
logging.error("Faction does not support offshore strike targets")
logging.error(
f"{self.faction_name} does not support offshore strike targets"
)
return
for position in self.control_point.preset_locations.offshore_strike_locations:
self.generate_strike_target_at("oil", position)
self.generate_building_at(TemplateCategory.Building, "oil", position)
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
def generate(self) -> bool:
self.generate_fob()
self.generate_armor_groups()
self.generate_factories()
self.generate_ammunition_depots()
self.generate_aa()
self.generate_ewrs()
self.generate_scenery_sites()
self.generate_strike_targets()
self.generate_offshore_strike_targets()
if self.faction.missiles:
self.generate_missile_sites()
if self.faction.coastal_defenses:
self.generate_coastal_sites()
return True
if super(FobGroundObjectGenerator, self).generate():
self.generate_fob()
return True
return False
def generate_fob(self) -> None:
category = "fob"
obj_name = self.control_point.name
template = random.choice(list(self.templates[category].values()))
point = self.control_point.position
# Pick from preset locations
object_id = 0
group_id = self.game.next_group_id()
# TODO: Create only one TGO per objective, each with multiple units.
for unit in template:
object_id += 1
template_point = Point(unit["offset"].x, unit["offset"].y)
g = BuildingGroundObject(
obj_name,
category,
group_id,
object_id,
point + template_point,
Heading.from_degrees(unit["heading"]),
self.control_point,
unit["type"],
is_fob_structure=True,
)
self.control_point.connected_objectives.append(g)
self.generate_building_at(
TemplateCategory.Building,
"fob",
PointWithHeading.from_point(
self.control_point.position, self.control_point.heading
),
)
class GroundObjectGenerator:
def __init__(self, game: Game, generator_settings: GeneratorSettings) -> None:
self.game = game
self.generator_settings = generator_settings
with open("resources/groundobject_templates.p", "rb") as f:
self.templates: GroundObjectTemplates = pickle.load(f)
def generate(self) -> None:
# Copied so we can remove items from the original list without breaking
@ -610,10 +503,10 @@ class GroundObjectGenerator:
)
elif isinstance(control_point, Fob):
generator = FobGroundObjectGenerator(
self.game, self.generator_settings, control_point, self.templates
self.game, self.generator_settings, control_point
)
else:
generator = AirbaseGroundObjectGenerator(
self.game, self.generator_settings, control_point, self.templates
self.game, self.generator_settings, control_point
)
return generator.generate()

View File

@ -6,21 +6,23 @@ 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
from typing import Iterator, List, TYPE_CHECKING, Union, Optional, Any
from dcs.vehicles import vehicle_map
from dcs.ships import ship_map
from dcs.mapping import Point
from dcs.triggers import TriggerZone
from dcs.unit import Unit
from dcs.vehicles import vehicle_map
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.unittype import UnitType
from ..point_with_heading import PointWithHeading
from ..utils import Distance, Heading, meters
if TYPE_CHECKING:
from gen.templates import UnitTemplate, GroupTemplate, TemplateRandomizer
from gen.templates import UnitTemplate, GroupTemplate
from .controlpoint import ControlPoint
from ..ato.flighttype import FlightType
@ -85,7 +87,7 @@ class GroundUnit:
position: PointWithHeading
ground_object: TheaterGroundObject
alive: bool = True
_dcs_type: Optional[GroundUnitType] = None
_unit_type: Optional[UnitType[Any]] = None
@staticmethod
def from_template(id: int, t: UnitTemplate, go: TheaterGroundObject) -> GroundUnit:
@ -98,17 +100,24 @@ class GroundUnit:
)
@property
def unit_type(self) -> Optional[GroundUnitType]:
if not self._dcs_type:
def unit_type(self) -> Optional[UnitType[Any]]:
if not self._unit_type:
try:
if self.type in vehicle_map:
dcs_unit_type = vehicle_map[self.type]
vehicle_type = db.vehicle_type_from_name(self.type)
self._unit_type = next(GroundUnitType.for_dcs_type(vehicle_type))
elif self.type in ship_map:
ship_type = db.ship_type_from_name(self.type)
# TODO Allow handling of Ships. This requires extension of UnitType
return None
elif (static_type := db.static_type_from_name(self.type)) is not None:
# TODO Allow handling of Statics
return None
else:
return None
self._dcs_type = next(GroundUnitType.for_dcs_type(dcs_unit_type))
except StopIteration:
return None
return self._dcs_type
return self._unit_type
def kill(self) -> None:
self.alive = False

576
gen/templates.py Normal file
View File

@ -0,0 +1,576 @@
from __future__ import annotations
import copy
import json
import logging
import random
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum
from typing import Iterator, Any, TYPE_CHECKING, Optional, Tuple, Union
from dcs import Point
from dcs.ships import ship_map
from dcs.unit import Unit
from dcs.unittype import UnitType
from dcs.vehicles import vehicle_map
from game.dcs.groundunittype import GroundUnitType
from game.theater.theatergroundobject import (
SamGroundObject,
EwrGroundObject,
BuildingGroundObject,
GroundGroup,
MissileSiteGroundObject,
ShipGroundObject,
CarrierGroundObject,
LhaGroundObject,
CoastalSiteGroundObject,
VehicleGroupGroundObject,
IadsGroundObject,
)
from game.point_with_heading import PointWithHeading
from game.utils import Heading
from game import db
if TYPE_CHECKING:
from game import Game
from game.factions.faction import Faction
from game.theater import TheaterGroundObject, ControlPoint
class TemplateEncoder(json.JSONEncoder):
def default(self, obj: Any) -> dict[str, Any]:
if hasattr(obj, "to_json"):
return obj.to_json()
else:
return obj.__dict__
class TemplateCategory(Enum):
AirDefence = "AirDefence" # Has subcategories for the AARange
Building = "Building" # Has subcategories from
Naval = "Naval" # Has subcategories lha, carrier, ship
Armor = "Armor"
Missile = "Missile"
Coastal = "Coastal"
@dataclass
class UnitTemplate:
name: str
type: str
position: Point
heading: int
@staticmethod
def from_unit(unit: Unit) -> UnitTemplate:
return UnitTemplate(
unit.name,
unit.type,
Point(int(unit.position.x), int(unit.position.y)),
int(unit.heading),
)
@staticmethod
def from_dict(d_unit: dict[str, Any]) -> UnitTemplate:
return UnitTemplate(
d_unit["name"],
d_unit["type"],
Point(d_unit["position"]["x"], d_unit["position"]["y"]),
d_unit["heading"],
)
@dataclass
class GroupTemplate:
name: str
units: list[UnitTemplate]
# Is Static group
static: bool = False
# Every group can only have one Randomizer
randomizer: Optional[TemplateRandomizer] = None
# Defines if this groupTemplate is required or not
optional: bool = False
@staticmethod
def from_dict(d_group: dict[str, Any]) -> GroupTemplate:
units = [UnitTemplate.from_dict(unit) for unit in d_group["units"]]
randomizer = (
TemplateRandomizer.from_dict(d_group["randomizer"])
if d_group["randomizer"]
else None
)
return GroupTemplate(
d_group["name"], units, d_group["static"], randomizer, d_group["optional"]
)
@property
def unit_types_count(self) -> dict[str, int]:
units: dict[str, int] = {}
for unit in self.units:
if unit.type in units:
units[unit.type] += 1
else:
units[unit.type] = 1
return units
@dataclass
class TemplateRandomizer:
# Selection of units to apply the randomization.
# If left empty the randomizer will be applied to all unit of the group
units: list[int] = field(default_factory=list)
# 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
count: Union[int, list[int]] = field(default=1)
# The randomizer can pick a random unit type from a faction list like
# frontline_units or air_defenses to allow faction sensitive randomization
faction_types: list[str] = field(default_factory=list)
# Only works for vehicle units. Allows to specify the type class of the unit.
# For example this allows to select frontline_units as faction_type and also define
# Shorads as class to only pick AntiAir from the list
type_classes: list[str] = field(default_factory=list)
# Allows to define the exact UnitTypes the randomizer picks from. these have to be
# the dcs_unit_types found in the pydcs arrays
unit_types: list[str] = field(default_factory=list)
# Runtime Attributes
_initialized: bool = False
_possible_types: list[str] = field(default_factory=list)
_random_unit_type: Optional[str] = None
_forced_unit_type: Optional[str] = None
_unit_counter: Optional[int] = None
def to_json(self) -> dict[str, Any]:
d = self.__dict__
# Do not serialize the runtime attributes
d.pop("_initialized", None)
d.pop("_possible_types", None)
d.pop("_random_unit_type", None)
d.pop("_forced_unit_type", None)
d.pop("_unit_counter", None)
return d
@staticmethod
def from_dict(d: dict[str, Any]) -> TemplateRandomizer:
return TemplateRandomizer(
d["units"],
d["count"],
d["faction_types"],
d["type_classes"],
d["unit_types"],
)
@property
def possible_ground_units(self) -> Iterator[GroundUnitType]:
for unit_type in self._possible_types:
if unit_type in vehicle_map:
dcs_unit_type = vehicle_map[unit_type]
try:
yield next(GroundUnitType.for_dcs_type(dcs_unit_type))
except StopIteration:
continue
def force_type(self, type: str) -> None:
self._forced_unit_type = type
def randomize(self) -> None:
self.randomize_unit_type()
self.reset_unit_counter()
def reset_unit_counter(self) -> None:
if isinstance(self.count, list):
count = random.choice(range(self.count[0], self.count[1]))
elif isinstance(self.count, int):
count = self.count
self._unit_counter = count
def init_randomization_for_faction(self, faction: Faction) -> None:
# Initializes the randomization
# This sets the random_unit_type and the random_unit_count
if self._initialized:
return
type_list = []
for faction_type in self.faction_types:
for unit_type in faction[faction_type]:
if isinstance(unit_type, GroundUnitType):
# GroundUnitType
type_list.append(unit_type.dcs_id)
elif issubclass(unit_type, UnitType):
# DCS Unit Type object
type_list.append(unit_type.id)
elif db.unit_type_from_name(unit_type):
# DCS Unit Type as string
type_list.append(unit_type)
else:
raise KeyError
if self.unit_types and self.faction_types:
# If Faction types were defined use unit_types as filter
filtered_type_list = [
unit_type for unit_type in type_list if unit_type in self.unit_types
]
type_list = filtered_type_list
else:
# If faction_types is not defined append the unit_types
for unit_type in self.unit_types:
type_list.append(unit_type)
if self.type_classes:
filtered_type_list = []
for unit_type in type_list:
if unit_type in vehicle_map:
dcs_type = vehicle_map[unit_type]
else:
continue
try:
ground_unit_type = next(GroundUnitType.for_dcs_type(dcs_type))
except (KeyError, StopIteration):
logging.error(f"Unit {unit_type} has no GroundUnitType")
continue
if (
ground_unit_type.unit_class
and ground_unit_type.unit_class.value in self.type_classes
):
filtered_type_list.append(unit_type)
type_list = filtered_type_list
self._possible_types = type_list
if self.randomize_unit_type():
self.reset_unit_counter()
self._initialized = True
@property
def unit_type(self) -> Optional[str]:
return self._random_unit_type
def randomize_unit_type(self) -> bool:
try:
self._random_unit_type = self._forced_unit_type or random.choice(
self._possible_types
)
except IndexError:
logging.warning("Can not initialize randomizer")
return False
return True
@property
def unit_count(self) -> int:
if not self._unit_counter:
self.reset_unit_counter()
return self._unit_counter or 1
def use_unit(self) -> None:
if self._unit_counter is None:
self.reset_unit_counter()
if self._unit_counter and self._unit_counter > 0:
self._unit_counter -= 1
else:
raise IndexError
@property
def unit_range(self) -> list[int]:
if len(self.units) > 1:
return list(range(self.units[0], self.units[1] + 1))
return self.units
class GroundObjectTemplate(ABC):
def __init__(
self, name: str, template_type: str = "", description: str = ""
) -> None:
self.name = name
self.template_type = template_type
self.description = description
self.groups: list[GroupTemplate] = []
@classmethod
def from_dict(cls, d_object: dict[str, Any]) -> GroundObjectTemplate:
template = cls(
d_object["name"], d_object["template_type"], d_object["description"]
)
for d_group in d_object["groups"]:
template.groups.append(GroupTemplate.from_dict(d_group))
return template
def generate(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
game: Game,
randomization: bool = True,
) -> TheaterGroundObject:
# Create the ground_object based on the type
ground_object = self._create_ground_object(name, position, control_point)
# Generate all groups using the randomization if it defined
for g_id, group in enumerate(self.groups):
tgo_group = GroundGroup.from_template(
game.next_group_id(),
group,
ground_object,
randomization,
)
# Set Group Name
tgo_group.name = f"{self.name} {g_id}"
# Assign UniqueID, name and align relative to ground_object
for u_id, unit in enumerate(tgo_group.units):
unit.id = game.next_unit_id()
unit.name = f"{self.name} {g_id}-{u_id}"
if isinstance(self, AirDefenceTemplate):
# Head SAM and EWR towards the center of the conflict
unit.position.heading = (
game.theater.heading_to_conflict_from(unit.position)
or unit.position.heading
)
unit.position = PointWithHeading.from_point(
Point(
ground_object.position.x + unit.position.x,
ground_object.position.y + unit.position.y,
),
unit.position.heading,
)
ground_object.groups.append(tgo_group)
return ground_object
@abstractmethod
def _create_ground_object(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
) -> TheaterGroundObject:
raise NotImplementedError
@property
def randomizable(self) -> bool:
# Returns True if any group of the template has a randomizer
return any(group_template.randomizer for group_template in self.groups)
def estimated_price_for(self, go: TheaterGroundObject) -> float:
# Price can only be estimated because of randomization
template_price = 0
for g_id, group in enumerate(self.groups):
tgo_group = GroundGroup.from_template(g_id, group, go)
for unit in tgo_group.units:
if unit.type in vehicle_map:
dcs_type = vehicle_map[unit.type]
try:
unit_type = next(GroundUnitType.for_dcs_type(dcs_type))
except StopIteration:
continue
template_price = template_price + unit_type.price
return template_price
@property
def size(self) -> int:
return sum([len(group.units) for group in self.groups])
@property
def min_size(self) -> int:
return self._size_for_randomized(True)
@property
def max_size(self) -> int:
return self._size_for_randomized(False)
def _size_for_randomized(self, min_size: bool) -> int:
size = 0
for group in self.groups:
for unit_id, unit in enumerate(group.units):
if group.randomizer and unit_id in group.randomizer.units:
if isinstance(group.randomizer.count, int):
size = size + group.randomizer.count
else:
size = size + group.randomizer.count[0 if min_size else 1]
else:
size = size + 1
return size
@property
def required_units(self) -> list[str]:
"""returns all required unit types by theyre dcs type id"""
# todo take care for randomizer
unit_types = []
for group in self.groups:
# this completly excludes randomized groups
if not group.optional and not group.randomizer:
for unit in group.units:
if unit.type not in unit_types:
unit_types.append(unit.type)
return unit_types
class AirDefenceTemplate(GroundObjectTemplate):
def _create_ground_object(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
) -> IadsGroundObject:
if self.template_type == "EWR":
return EwrGroundObject(name, position, control_point)
elif self.template_type in ["Long", "Medium", "Short", "AAA"]:
return SamGroundObject(name, position, control_point)
raise RuntimeError(
f" No Template Definition for AirDefence with subcategory {self.template_type}"
)
class BuildingTemplate(GroundObjectTemplate):
def _create_ground_object(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
) -> BuildingGroundObject:
return BuildingGroundObject(
name,
self.template_type,
position,
Heading.from_degrees(0),
control_point,
self.template_type == "fob",
)
class NavalTemplate(GroundObjectTemplate):
def _create_ground_object(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
) -> TheaterGroundObject:
if self.template_type == "ship":
return ShipGroundObject(name, position, control_point)
elif self.template_type == "carrier":
return CarrierGroundObject(name, control_point)
elif self.template_type == "lha":
return LhaGroundObject(name, control_point)
raise NotImplementedError
class CoastalTemplate(GroundObjectTemplate):
def _create_ground_object(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
) -> TheaterGroundObject:
return CoastalSiteGroundObject(name, position, control_point, position.heading)
class ArmorTemplate(GroundObjectTemplate):
def _create_ground_object(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
) -> TheaterGroundObject:
return VehicleGroupGroundObject(name, position, control_point)
class MissileTemplate(GroundObjectTemplate):
def _create_ground_object(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
) -> TheaterGroundObject:
return MissileSiteGroundObject(name, position, control_point)
TEMPLATE_TYPES = {
TemplateCategory.AirDefence: AirDefenceTemplate,
TemplateCategory.Building: BuildingTemplate,
TemplateCategory.Naval: NavalTemplate,
TemplateCategory.Armor: ArmorTemplate,
TemplateCategory.Missile: MissileTemplate,
TemplateCategory.Coastal: CoastalTemplate,
}
class GroundObjectTemplates:
# list of templates per category. e.g. AA or similar
_templates: dict[TemplateCategory, list[GroundObjectTemplate]]
def __init__(self) -> None:
self._templates = {}
@property
def templates(self) -> Iterator[tuple[TemplateCategory, GroundObjectTemplate]]:
for category, templates in self._templates.items():
for template in templates:
yield category, template
@classmethod
def from_json(cls, template_file: str) -> GroundObjectTemplates:
# Rebuild the TemplatesObject from the json dict
obj = GroundObjectTemplates()
with open(template_file, "r") as f:
json_templates: dict[str, list[dict[str, Any]]] = json.load(f)
for category, templates in json_templates.items():
for d_template in templates:
template = TEMPLATE_TYPES[TemplateCategory(category)].from_dict(
d_template
)
obj.add_template(TemplateCategory(category), template)
return obj
def to_json(self) -> dict[str, Any]:
return {
category.value: templates for category, templates in self._templates.items()
}
@property
def all(self) -> Iterator[GroundObjectTemplate]:
for templates in self._templates.values():
yield from templates
def by_name(self, template_name: str) -> Optional[GroundObjectTemplate]:
for template in self.all:
if template.name == template_name:
return template
return None
def by_category_and_name(
self, category: TemplateCategory, template_name: str
) -> Optional[GroundObjectTemplate]:
if category in self._templates:
for template in self._templates[category]:
if template.name == template_name:
return template
return None
def add_template(
self, category: TemplateCategory, template: GroundObjectTemplate
) -> None:
if category not in self._templates:
self._templates[category] = [template]
else:
self._templates[category].append(template)
def for_category(
self, category: TemplateCategory, sub_category: Optional[str] = None
) -> Iterator[GroundObjectTemplate]:
if category not in self._templates:
return None
for template in self._templates[category]:
if not sub_category or template.template_type == sub_category:
yield template

View File

@ -0,0 +1,259 @@
import logging
from typing import Optional
from PySide2.QtCore import Signal
from PySide2.QtGui import Qt
from PySide2.QtWidgets import (
QComboBox,
QDialog,
QGridLayout,
QGroupBox,
QLabel,
QMessageBox,
QPushButton,
QSpinBox,
QVBoxLayout,
QCheckBox,
)
from game import Game
from game.point_with_heading import PointWithHeading
from game.theater import TheaterGroundObject
from game.theater.theatergroundobject import (
VehicleGroupGroundObject,
SamGroundObject,
EwrGroundObject,
GroundGroup,
)
from gen.templates import (
TemplateCategory,
GroundObjectTemplate,
GroupTemplate,
)
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
class QGroundObjectGroupTemplate(QGroupBox):
group_template_changed = Signal(GroupTemplate)
# UI to show one GroupTemplate and configure the TemplateRandomizer for it
# one row: [Required | Unit Selector | Amount | Price]
# If the group is not randomizable: Just view labels instead of edit fields
def __init__(self, group_id: int, group_template: GroupTemplate) -> None:
super(QGroundObjectGroupTemplate, self).__init__(str(group_id + 1))
self.group_template = group_template
self.group_layout = QGridLayout()
self.setLayout(self.group_layout)
self.amount_selector = QSpinBox()
self.unit_selector = QComboBox()
self.group_selector = QCheckBox()
self.group_selector.setChecked(True)
self.group_selector.setEnabled(self.group_template.optional)
if self.group_template.randomizer:
# Group can be randomized
for unit in self.group_template.randomizer.possible_ground_units:
self.unit_selector.addItem(f"{unit} [${unit.price}M]", userData=unit)
self.group_layout.addWidget(
self.unit_selector, 0, 0, alignment=Qt.AlignRight
)
self.group_layout.addWidget(
self.amount_selector, 0, 1, alignment=Qt.AlignRight
)
self.amount_selector.setMinimum(1)
self.amount_selector.setMaximum(len(self.group_template.units))
self.amount_selector.setValue(self.group_template.randomizer.unit_count)
self.on_group_changed()
else:
# Group can not be randomized so just show the group info
group_info = QVBoxLayout()
for unit_type, count in self.group_template.unit_types_count.items():
group_info.addWidget(
QLabel(f"{count}x {unit_type}"), alignment=Qt.AlignLeft
)
self.group_layout.addLayout(group_info, 0, 0, 1, 2)
self.group_layout.addWidget(self.group_selector, 0, 2, alignment=Qt.AlignRight)
self.amount_selector.valueChanged.connect(self.on_group_changed)
self.unit_selector.currentIndexChanged.connect(self.on_group_changed)
self.group_selector.stateChanged.connect(self.on_group_changed)
def on_group_changed(self) -> None:
unit_type = self.unit_selector.itemData(self.unit_selector.currentIndex())
count = self.amount_selector.value() if self.group_selector.isChecked() else 0
self.group_template.randomizer.count = count
self.group_template.randomizer.force_type(unit_type.dcs_id)
self.group_template_changed.emit(self.group_template)
class QGroundObjectTemplateLayout(QGroupBox):
def __init__(
self,
game: Game,
ground_object: TheaterGroundObject,
template_changed_signal: Signal(GroundObjectTemplate),
current_group_value: int,
):
super(QGroundObjectTemplateLayout, self).__init__("Groups:")
# Connect to the signal to handle template updates
self.game = game
self.ground_object = ground_object
self.template_changed_signal = template_changed_signal
self.template_changed_signal.connect(self.load_for_template)
self.template: Optional[GroundObjectTemplate] = None
self.current_group_value = current_group_value
self.buy_button = QPushButton("Buy")
self.buy_button.clicked.connect(self.buy_group)
self.template_layout = QGridLayout()
self.setLayout(self.template_layout)
self.template_grid = QGridLayout()
self.template_layout.addLayout(self.template_grid, 0, 0, 1, 2)
self.template_layout.addWidget(self.buy_button, 1, 1)
stretch = QVBoxLayout()
stretch.addStretch()
self.template_layout.addLayout(stretch, 2, 0)
def load_for_template(self, template: GroundObjectTemplate) -> None:
self.template = template
# Clean the current grid
for id in range(self.template_grid.count()):
self.template_grid.itemAt(id).widget().deleteLater()
for g_id, group in enumerate(template.groups):
group_row = QGroundObjectGroupTemplate(g_id, group)
group_row.group_template_changed.connect(self.group_template_changed)
self.template_grid.addWidget(group_row)
self.update_price()
def group_template_changed(self, group_template: GroupTemplate) -> None:
self.update_price()
def update_price(self) -> None:
price = "$" + str(self.template.estimated_price_for(self.ground_object))
if self.template.randomizable:
price = "~" + price
self.buy_button.setText(f"Buy [{price}M][-${self.current_group_value}M]")
def buy_group(self):
if not self.template:
return
groups = self.generate_groups()
price = 0
for group in groups:
for unit in group.units:
if unit.unit_type:
price += unit.unit_type.price
price -= self.current_group_value
if price > self.game.blue.budget:
self.error_money()
self.close()
return
else:
self.game.blue.budget -= price
self.ground_object.groups = groups
# Replan redfor missions
self.game.initialize_turn(for_red=True, for_blue=False)
GameUpdateSignal.get_instance().updateGame(self.game)
def error_money(self):
msg = QMessageBox()
msg.setIcon(QMessageBox.Information)
msg.setText("Not enough money to buy these units !")
msg.setWindowTitle("Not enough money")
msg.setStandardButtons(QMessageBox.Ok)
msg.setWindowFlags(Qt.WindowStaysOnTopHint)
msg.exec_()
self.close()
def generate_groups(self) -> list[GroundGroup]:
go = self.template.generate(
self.ground_object.name,
PointWithHeading.from_point(
self.ground_object.position, self.ground_object.heading
),
self.ground_object.control_point,
self.game,
)
return go.groups
class QGroundObjectBuyMenu(QDialog):
template_changed_signal = Signal(GroundObjectTemplate)
def __init__(
self,
parent,
ground_object: TheaterGroundObject,
game: Game,
current_group_value: int,
):
super(QGroundObjectBuyMenu, self).__init__(parent)
self.setMinimumWidth(350)
self.setWindowTitle("Buy ground object @ " + ground_object.obj_name)
self.setWindowIcon(EVENT_ICONS["capture"])
self.mainLayout = QGridLayout()
self.setLayout(self.mainLayout)
self.template_selector = QComboBox()
self.template_selector.currentIndexChanged.connect(self.template_changed)
# Get the templates and fill the combobox
template_sub_category = None
if isinstance(ground_object, SamGroundObject):
template_category = TemplateCategory.AirDefence
elif isinstance(ground_object, VehicleGroupGroundObject):
template_category = TemplateCategory.Armor
elif isinstance(ground_object, EwrGroundObject):
template_category = TemplateCategory.AirDefence
template_sub_category = "EWR"
else:
raise RuntimeError
for template in game.blue.faction.templates.for_category(
template_category, template_sub_category
):
self.template_selector.addItem(template.name, userData=template)
template_selector_layout = QGridLayout()
template_selector_layout.addWidget(QLabel("Template :"), 0, 0, Qt.AlignLeft)
template_selector_layout.addWidget(
self.template_selector, 0, 1, alignment=Qt.AlignRight
)
self.mainLayout.addLayout(template_selector_layout, 0, 0)
self.template_layout = QGroundObjectTemplateLayout(
game, ground_object, self.template_changed_signal, current_group_value
)
self.mainLayout.addWidget(self.template_layout, 1, 0)
self.setLayout(self.mainLayout)
# Update UI
self.template_changed()
def template_changed(self):
template = self.template_selector.itemData(
self.template_selector.currentIndex()
)
self.template_changed_signal.emit(template)

View File

@ -1,17 +1,12 @@
import logging
from typing import List, Optional
from PySide2.QtGui import Qt
from PySide2.QtWidgets import (
QComboBox,
QDialog,
QGridLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QMessageBox,
QPushButton,
QSpinBox,
QVBoxLayout,
)
from dcs import Point, vehicles
@ -19,6 +14,7 @@ from dcs import Point, vehicles
from game import Game
from game.config import REWARDS
from game.data.building_data import FORTIFICATION_BUILDINGS
from game.data.groundunitclass import GroundUnitClass
from game.dcs.groundunittype import GroundUnitType
from game.theater import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import (
@ -27,13 +23,11 @@ from game.theater.theatergroundobject import (
SamGroundObject,
VehicleGroupGroundObject,
)
from gen.defenses.armor_group_generator import generate_armor_group_of_type_and_size
from gen.sam.ewr_group_generator import get_faction_possible_ewrs_generator
from gen.sam.sam_group_generator import get_faction_possible_sams_generator
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.QBudgetBox import QBudgetBox
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.groundobject.QBuildingInfo import QBuildingInfo
from qt_ui.windows.groundobject.QGroundObjectBuyMenu import QGroundObjectBuyMenu
class QGroundObjectMenu(QDialog):
@ -41,17 +35,12 @@ class QGroundObjectMenu(QDialog):
self,
parent,
ground_object: TheaterGroundObject,
buildings: Optional[List[TheaterGroundObject]],
cp: ControlPoint,
game: Game,
):
super().__init__(parent)
self.setMinimumWidth(350)
self.ground_object = ground_object
if buildings is None:
self.buildings = []
else:
self.buildings = buildings
self.cp = cp
self.game = game
self.setWindowTitle(
@ -231,225 +220,7 @@ class QGroundObjectMenu(QDialog):
GameUpdateSignal.get_instance().updateGame(self.game)
def buy_group(self):
self.subwindow = QBuyGroupForGroundObjectDialog(
self, self.ground_object, self.cp, self.game, self.total_value
self.subwindow = QGroundObjectBuyMenu(
self, self.ground_object, self.game, self.total_value
)
self.subwindow.show()
class QBuyGroupForGroundObjectDialog(QDialog):
def __init__(
self,
parent,
ground_object: TheaterGroundObject,
cp: ControlPoint,
game: Game,
current_group_value: int,
):
super(QBuyGroupForGroundObjectDialog, self).__init__(parent)
self.setMinimumWidth(350)
self.ground_object = ground_object
self.cp = cp
self.game = game
self.current_group_value = current_group_value
self.setWindowTitle("Buy units @ " + self.ground_object.obj_name)
self.setWindowIcon(EVENT_ICONS["capture"])
self.buySamButton = QPushButton("Buy")
self.buyArmorButton = QPushButton("Buy")
self.buySamLayout = QGridLayout()
self.buyArmorLayout = QGridLayout()
self.amount = QSpinBox()
self.buyArmorCombo = QComboBox()
self.samCombo = QComboBox()
self.buySamBox = QGroupBox("Buy SAM site :")
self.buyArmorBox = QGroupBox("Buy defensive position :")
faction = self.game.blue.faction
# Sams
possible_sams = get_faction_possible_sams_generator(faction)
for sam in possible_sams:
# Pre Generate SAM to get the real price
generator = sam(self.game, self.ground_object)
generator.generate()
self.samCombo.addItem(
generator.name + " [$" + str(generator.price) + "M]", userData=generator
)
self.samCombo.currentIndexChanged.connect(self.samComboChanged)
self.buySamLayout.addWidget(QLabel("Site Type :"), 0, 0, Qt.AlignLeft)
self.buySamLayout.addWidget(self.samCombo, 0, 1, alignment=Qt.AlignRight)
self.buySamLayout.addWidget(self.buySamButton, 1, 1, alignment=Qt.AlignRight)
stretch = QVBoxLayout()
stretch.addStretch()
self.buySamLayout.addLayout(stretch, 2, 0)
self.buySamButton.clicked.connect(self.buySam)
# EWRs
buy_ewr_box = QGroupBox("Buy EWR:")
buy_ewr_layout = QGridLayout()
buy_ewr_box.setLayout(buy_ewr_layout)
buy_ewr_layout.addWidget(QLabel("Radar type:"), 0, 0, Qt.AlignLeft)
self.ewr_selector = QComboBox()
buy_ewr_layout.addWidget(self.ewr_selector, 0, 1, alignment=Qt.AlignRight)
ewr_types = get_faction_possible_ewrs_generator(faction)
for ewr_type in ewr_types:
# Pre Generate to get the real price
generator = ewr_type(self.game, self.ground_object)
generator.generate()
self.ewr_selector.addItem(
generator.name() + " [$" + str(generator.price) + "M]",
userData=generator,
)
self.ewr_selector.currentIndexChanged.connect(self.on_ewr_selection_changed)
self.buy_ewr_button = QPushButton("Buy")
self.buy_ewr_button.clicked.connect(self.buy_ewr)
buy_ewr_layout.addWidget(self.buy_ewr_button, 1, 1, alignment=Qt.AlignRight)
stretch = QVBoxLayout()
stretch.addStretch()
buy_ewr_layout.addLayout(stretch, 2, 0)
# Armored units
for unit in set(faction.ground_units):
self.buyArmorCombo.addItem(f"{unit} [${unit.price}M]", userData=unit)
self.buyArmorCombo.currentIndexChanged.connect(self.armorComboChanged)
self.amount.setMinimum(2)
self.amount.setMaximum(8)
self.amount.setValue(2)
self.amount.valueChanged.connect(self.amountComboChanged)
self.buyArmorLayout.addWidget(QLabel("Unit type :"), 0, 0, Qt.AlignLeft)
self.buyArmorLayout.addWidget(self.buyArmorCombo, 0, 1, alignment=Qt.AlignRight)
self.buyArmorLayout.addWidget(
QLabel("Group size :"), 1, 0, alignment=Qt.AlignLeft
)
self.buyArmorLayout.addWidget(self.amount, 1, 1, alignment=Qt.AlignRight)
self.buyArmorLayout.addWidget(
self.buyArmorButton, 2, 1, alignment=Qt.AlignRight
)
stretch2 = QVBoxLayout()
stretch2.addStretch()
self.buyArmorLayout.addLayout(stretch2, 3, 0)
self.buyArmorButton.clicked.connect(self.buyArmor)
# Do layout
self.buySamBox.setLayout(self.buySamLayout)
self.buyArmorBox.setLayout(self.buyArmorLayout)
self.mainLayout = QHBoxLayout()
if isinstance(self.ground_object, SamGroundObject):
self.mainLayout.addWidget(self.buySamBox)
elif isinstance(self.ground_object, VehicleGroupGroundObject):
self.mainLayout.addWidget(self.buyArmorBox)
elif isinstance(self.ground_object, EwrGroundObject):
self.mainLayout.addWidget(buy_ewr_box)
self.setLayout(self.mainLayout)
try:
self.samComboChanged(0)
self.armorComboChanged(0)
self.on_ewr_selection_changed(0)
except:
pass
def samComboChanged(self, index):
self.buySamButton.setText(
"Buy [$"
+ str(self.samCombo.itemData(index).price)
+ "M] [-$"
+ str(self.current_group_value)
+ "M]"
)
def on_ewr_selection_changed(self, index):
ewr = self.ewr_selector.itemData(index)
self.buy_ewr_button.setText(
f"Buy [${ewr.price}M][-${self.current_group_value}M]"
)
def armorComboChanged(self, index):
unit_type = self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())
price = unit_type.price * self.amount.value()
self.buyArmorButton.setText(f"Buy [${price}M][-${self.current_group_value}M]")
def amountComboChanged(self):
unit_type = self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())
price = unit_type.price * self.amount.value()
self.buyArmorButton.setText(f"Buy [${price}M][-${self.current_group_value}M]")
def buyArmor(self):
logging.info("Buying Armor ")
utype = self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())
price = utype.price * self.amount.value() - self.current_group_value
if price > self.game.blue.budget:
self.error_money()
self.close()
return
else:
self.game.blue.budget -= price
# Generate Armor
group = generate_armor_group_of_type_and_size(
self.game, self.ground_object, utype, int(self.amount.value())
)
self.ground_object.groups = [group]
# Replan redfor missions
self.game.initialize_turn(for_red=True, for_blue=False)
GameUpdateSignal.get_instance().updateGame(self.game)
def buySam(self):
sam_generator = self.samCombo.itemData(self.samCombo.currentIndex())
price = sam_generator.price - self.current_group_value
if price > self.game.blue.budget:
self.error_money()
return
else:
self.game.blue.budget -= price
self.ground_object.groups = list(sam_generator.groups)
# Replan redfor missions
self.game.initialize_turn(for_red=True, for_blue=False)
GameUpdateSignal.get_instance().updateGame(self.game)
def buy_ewr(self):
ewr_generator = self.ewr_selector.itemData(self.ewr_selector.currentIndex())
price = ewr_generator.price - self.current_group_value
if price > self.game.blue.budget:
self.error_money()
return
else:
self.game.blue.budget -= price
self.ground_object.groups = [ewr_generator.vg]
# Replan redfor missions
self.game.initialize_turn(for_red=True, for_blue=False)
GameUpdateSignal.get_instance().updateGame(self.game)
def error_money(self):
msg = QMessageBox()
msg.setIcon(QMessageBox.Information)
msg.setText("Not enough money to buy these units !")
msg.setWindowTitle("Not enough money")
msg.setStandardButtons(QMessageBox.Ok)
msg.setWindowFlags(Qt.WindowStaysOnTopHint)
msg.exec_()
self.close()

Binary file not shown.

View File

@ -1,59 +0,0 @@
import pickle
import typing
from dcs.mission import Mission
from dcs.mapping import Point
from dcs.unit import *
from dcs.statics import warehouse_map, fortification_map
def load_templates():
temp_mis = Mission()
temp_mis.load_file("resources/tools/groundobject_templates.miz")
groups = {} # type: typing.Dict[str, typing.Dict[int, typing.List[Static]]]
for static_group in (
temp_mis.country("USA").static_group
+ temp_mis.country("USAF Aggressors").static_group
):
for static in static_group.units:
static_name = str(static.name).split()[0]
tpl_name, tpl_idx = static_name[:-1], int(static_name[-1])
groups[tpl_name] = groups.get(tpl_name, {})
groups[tpl_name][tpl_idx] = groups[tpl_name].get(tpl_idx, [])
groups[tpl_name][tpl_idx].append(static)
tpls = {name: {idx: [] for idx in groups[name].keys()} for name in groups.keys()}
for category_name, category_groups in groups.items():
for idx, static_groups in category_groups.items():
dist = -1
a, b = None, None
for aa in static_groups:
for bb in static_groups:
if aa.position.distance_to_point(bb.position) > dist:
dist = aa.position.distance_to_point(bb.position)
a = aa
b = bb
center = a.position.point_from_heading(
a.position.heading_between_point(b.position), dist / 2
)
for static in static_groups:
tpls[category_name][idx].append(
{
"type": static.type,
"offset": Point(
center.x - static.position.x, center.y - static.position.y
),
"heading": static.heading,
}
)
tpls["aa"] = {0: [{"type": "AA", "offset": Point(0, 0), "heading": 0}]}
return tpls
with open("resources/groundobject_templates.p", "wb") as f:
pickle.dump(load_templates(), f)

View File

@ -0,0 +1,785 @@
from __future__ import annotations
import argparse
import json
import math
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Optional, Any, Iterator
from tabulate import tabulate
import dcs
from dcs import Point
from game import Game
from game.campaignloader import CampaignAirWingConfig
from game.db import FACTIONS
from game.missiongenerator.tgogenerator import (
GroundObjectGenerator,
)
from game.point_with_heading import PointWithHeading
from game.settings import Settings
from game.theater import CaucasusTheater, OffMapSpawn
from game.unitmap import UnitMap
from game.utils import Heading
from gen.to_remove.armored_group_generator import (
FixedSizeArmorGroupGenerator,
FixedSizeArmorGroupGeneratorWithAA,
)
from gen.to_remove.carrier_group import (
CarrierGroupGenerator,
CarrierStrikeGroup8Generator,
)
from gen.to_remove.lha_group import LHAGroupGenerator
from gen.to_remove.ship_group_generator import SHIP_MAP
from gen.to_remove.coastal_group_generator import COASTAL_MAP
from gen.templates import (
GroundObjectTemplates,
TemplateCategory,
GroupTemplate,
UnitTemplate,
TemplateEncoder,
MissileTemplate,
BuildingTemplate,
CoastalTemplate,
NavalTemplate,
TemplateRandomizer,
ArmorTemplate,
TEMPLATE_TYPES,
AirDefenceTemplate,
)
from gen.to_remove.missiles_group_generator import MISSILES_MAP
from gen.to_remove.airdefensegroupgenerator import AirDefenseGroupGenerator
from gen.to_remove.ewr_group_generator import EWR_MAP
from gen.to_remove.ewrs import EwrGenerator
from gen.to_remove.sam_group_generator import SAM_MAP
from gen.naming import namegen
from qt_ui import liberation_install
TEMPLATES_MIZ = "resources/templates/templates.miz"
MIGRATE_MIZ = "resources/tools/groundobject_templates.miz"
TEMPLATES_BLOB = "resources/templates/templates.json"
TEMPLATES_MAP = "resources/templates/template_map.json"
TABLE_FILE = "doc/templates/template_list.md"
@dataclass
class GroupTemplateMapping:
name: str
optional: bool
unit_types: list[str]
unit_classes: list[UnitClass]
def to_json(self) -> dict[str, Any]:
d = self.__dict__
if not self.optional:
d.pop("optional")
if not self.unit_types:
d.pop("unit_types")
if not self.unit_classes:
d.pop("unit_classes")
return d
@staticmethod
def from_dict(d: dict[str, Any]) -> GroupTemplateMapping:
optional = d["optional"] if "optional" in d else False
return GroupTemplateMapping(
d["name"], optional, d["unit_types"], d["unit_classes"]
)
@dataclass
class TemplateMapping:
name: str
template_type: str
description: str
groups: list[GroupTemplateMapping]
statics: list[str]
static_mapping: Optional[GroupTemplateMapping] = None
def to_json(self) -> dict[str, Any]:
d = self.__dict__
if not self.static_mapping:
d.pop("static_mapping")
return d
@staticmethod
def from_dict(d: dict[str, Any]) -> TemplateMapping:
groups = [GroupTemplateMapping.from_dict(group) for group in d["groups"]]
static_mapping = (
GroupTemplateMapping.from_dict(d["static_mapping"])
if "static_mapping" in d
else None
)
return TemplateMapping(
d["name"],
d["template_type"],
d["description"],
groups,
d["statics"],
static_mapping,
)
class TemplateMap:
_map: dict[str, list[TemplateMapping]] = {}
def __init__(self):
self._map = {}
def __getitem__(self, item: str) -> list[TemplateMapping]:
return self._map[item]
def __setitem__(self, key, value) -> None:
self._map[key] = value
def to_json(self) -> dict[str, Any]:
return self._map
def mapping_for_template(self, template_name: str) -> tuple[str, TemplateMapping]:
for category, mappings in self._map.items():
for mapping in mappings:
if mapping.name == template_name:
return category, mapping
raise RuntimeError(f"No mapping for template {template_name}")
def mapping_for_group(
self, group_name: str
) -> tuple[str, TemplateMapping, Optional[GroupTemplateMapping]]:
for category, mappings in self._map.items():
for mapping in mappings:
for group_mapping in mapping.groups:
if group_mapping.name == group_name:
return category, mapping, group_mapping
if group_name in mapping.statics:
return category, mapping, mapping.static_mapping
raise RuntimeError(f"No mapping for group {group_name}")
@property
def mappings(self) -> Iterator[TemplateMapping]:
for mapping in self._map.values():
yield mapping
def import_templates(
miz_file: str, template_map_file: str, target_file: str, table_file: str
) -> None:
"""Imports the template miz and the template_map as json into liberation"""
temp_mis = dcs.Mission()
temp_mis.load_file(miz_file)
templates = GroundObjectTemplates()
with open(template_map_file, "r") as f:
template_map_json = json.load(f)
template_map = TemplateMap()
for name, mappings in template_map_json.items():
template_map[TemplateCategory(name)] = []
for mapping in mappings:
template_map[TemplateCategory(name)].append(
TemplateMapping.from_dict(mapping)
)
template_position: dict[str, Point] = {}
for static_group in temp_mis.country("USA").static_group:
category, mapping, group_mapping = template_map.mapping_for_group(
static_group.name
)
template = templates.by_category_and_name(
TemplateCategory(category), mapping.name
)
static_template = None
if not template:
template = BuildingTemplate(
mapping.name, mapping.template_type, mapping.description
)
static_template = GroupTemplate(mapping.name, [], True)
if group_mapping:
static_template.randomizer = group_mapping.randomizer
static_template.optional = group_mapping.optional
template.groups.append(static_template)
templates.add_template(TemplateCategory(category), template)
else:
for group in template.groups:
if mapping.name in group.name:
static_template = group
if not static_template:
raise RuntimeError(f"No mapping for group {static_group.name}")
for i, unit in enumerate(static_group.units):
unit_template = UnitTemplate.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]
)
static_template.units.append(unit_template)
pass
for vehicle_group in (
temp_mis.country("USA").vehicle_group + temp_mis.country("USA").ship_group
):
category, mapping, group_mapping = template_map.mapping_for_group(
vehicle_group.name
)
template = templates.by_category_and_name(
TemplateCategory(category), mapping.name
)
if not template:
template = TEMPLATE_TYPES[TemplateCategory(category)](
mapping.name, mapping.template_type, mapping.description
)
templates.add_template(TemplateCategory(category), template)
for i, unit in enumerate(vehicle_group.units):
group_template = None
for group in template.groups:
if group.name == vehicle_group.name:
group_template = group
if not group_template:
group_template = GroupTemplate(
vehicle_group.name,
[],
False,
group_mapping.randomizer,
group_mapping.optional,
)
template.groups.append(group_template)
unit_template = UnitTemplate.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)
pass
with open(target_file, "w") as f:
json.dump(templates.to_json(), f, cls=TemplateEncoder, indent="\t")
# print Table
print_table(target_file, table_file)
def export_templates(
templates_file: str, miz_file: str, template_map_file: str
) -> None:
"""Exports the templates.json to the templates.miz and the mapping"""
# This function will only be used on mass jobs or specific fine tuning
templates = GroundObjectTemplates.from_json(templates_file)
# Prepare game
theater = CaucasusTheater()
initial_ground_position = Point(-500000, 250000)
initial_water_position = Point(-350000, 250000)
control_point_ground = OffMapSpawn(1, "Spawn Ground", initial_ground_position, True)
control_point_water = OffMapSpawn(2, "Spawn Water", initial_water_position, True)
theater.add_controlpoint(control_point_ground)
theater.add_controlpoint(control_point_water)
game = Game(
FACTIONS["Bluefor Modern"],
FACTIONS["Russia 2010"],
theater,
CampaignAirWingConfig({control_point_ground: [], control_point_water: []}),
datetime.today(),
Settings(),
10000,
10000,
)
m = dcs.Mission(game.theater.terrain)
country = m.country("USA")
unit_map = UnitMap()
template_map = TemplateMap()
offset_x = 0
offset_y = 0
for count, template_category in enumerate(TemplateCategory):
template_map[template_category.value] = []
temmplates_for_category = list(templates.for_category(template_category))
# Define the offset / separation
category_separation = 10000
group_separation = 5000
if template_category in [TemplateCategory.Naval]:
initial_position = initial_water_position
control_point = control_point_water
else:
initial_position = initial_ground_position
control_point = control_point_ground
current_separation = offset_y + category_separation
offset_x = 0
offset_y = current_separation
max_cols = int(math.sqrt(len(temmplates_for_category)))
for template in temmplates_for_category:
mapping = TemplateMapping(
template.name,
template.template_type,
template.description,
[],
[],
)
position = Point(
initial_position.x + offset_x, initial_position.y + offset_y
)
# Initialize the template
for group_template in template.groups:
if group_template.randomizer:
group_template.randomizer.init_randomization_for_faction(
game.blue.faction
)
ground_object = template.generate(
template.name,
PointWithHeading.from_point(position, Heading.from_degrees(0)),
control_point,
game,
False, # Do not use randomization during export!
)
for g_id, group in enumerate(ground_object.groups):
group.name = f"{template.name} {g_id}"
for u_id, unit in enumerate(group.units):
unit.name = f"{template.name} {g_id}-{u_id}"
group_template = template.groups[g_id]
group_mapping = GroupTemplateMapping(
group.name, group_template.optional, group_template.randomizer
)
if group.static_group:
for unit in group.units:
mapping.statics.append(unit.name)
# No mapping as we have no randomization yet
else:
mapping.groups.append(group_mapping)
generator = GroundObjectGenerator(ground_object, country, game, m, unit_map)
generator.generate(unique_name=False) # Prevent the ID prefix
if ((offset_y - current_separation) / group_separation) < max_cols:
offset_y += group_separation
else:
offset_y = current_separation
offset_x += group_separation
template_map[template_category.name].append(mapping)
# Dump the template_map as json
with open(template_map_file, "w") as f:
json.dump(template_map, f, cls=TemplateEncoder, sort_keys=True, indent="\t")
m.save(miz_file)
def print_table(templates_file: str, table_file: str) -> None:
# Category # Sub Category # Template Name # Unit_types
templates = GroundObjectTemplates.from_json(templates_file)
table = []
for template_category in TemplateCategory:
category_templates = list(templates.for_category(template_category))
for template in sorted(
category_templates, key=lambda x: (x.template_type, x.name)
):
groups = []
options = []
for group in template.groups:
units = []
group_options = []
for unit in group.units:
units.append(unit.type)
groups.append("<li>" + ", ".join(units) + "</li>")
if group.randomizer:
# Add More info about the randomizer?
group_options.append("Randomizer")
if group.static:
group_options.append("Static")
if group_options:
options.append("<li>" + ", ".join(group_options) + "</li>")
table.append(
[
template_category.value,
template.template_type,
template.name,
"<ul>" + "".join(groups) + "</ul>",
"<ul>" + "".join(options) + "</ul>",
]
)
table_str = tabulate(
table,
headers=["Category", "Sub-Category", "Template", "Groups & Units", "Options"],
tablefmt="github",
)
with open(table_file, "w", encoding="utf-8") as fdata:
fdata.write(table_str)
def replace(string: str, replacement) -> str:
for old_name, new_name in replacement:
string = string.replace(old_name, new_name)
return string
def update_factions(generator_names) -> None:
folder: Path = Path("./resources/factions/")
factions = [f for f in folder.glob("*.json") if f.is_file()]
for f in factions:
with open(f, "r", encoding="utf-8") as fdata:
data = json.load(fdata)
updated_faction = {}
for key, item in data.items():
if isinstance(item, list):
updated = []
for string in item:
updated.append(replace(string, generator_names))
updated_faction[key] = updated
elif isinstance(item, str):
updated_faction[key] = replace(item, generator_names)
else:
updated_faction[key] = item
with open(f, "w", encoding="utf-8") as fdata:
json.dump(updated_faction, fdata, indent=2)
print("\n \n Faction Updates:")
print(
tabulate(
generator_names,
headers=["Previous Value", "New Value"],
tablefmt="github",
)
)
# def migrate_generators_to_templates(
# input_miz: str,
# templates_file: str,
# miz_file: str,
# template_map_file: str,
# table_file: str,
# ) -> None:
#
# templates = GroundObjectTemplates()
#
# theater = CaucasusTheater()
#
# initial_position = Point(0, 0)
# control_point = OffMapSpawn(1, "Spawn", initial_position, True)
# theater.add_controlpoint(control_point)
#
# game = Game(
# FACTIONS["Bluefor Modern"],
# FACTIONS["Russia 2010"],
# theater,
# CampaignAirWingConfig({control_point: []}),
# datetime.today(),
# Settings(),
# 10000,
# 10000,
# )
#
# generators: dict[TemplateCategory, dict[str, Any]] = {
# TemplateCategory.AirDefence: SAM_MAP,
# TemplateCategory.Naval: SHIP_MAP,
# TemplateCategory.Missile: MISSILES_MAP,
# TemplateCategory.Coastal: COASTAL_MAP,
# TemplateCategory.Armor: {},
# }
#
# # Only use one EWR generator. The differnt units will be placed as randomizer
# generators[TemplateCategory.AirDefence]["EWRGenerator"] = EwrGenerator
#
# generators[TemplateCategory.Naval]["CarrierGroupGenerator"] = CarrierGroupGenerator
# generators[TemplateCategory.Naval][
# "CarrierStrikeGroup8Generator"
# ] = CarrierStrikeGroup8Generator
# generators[TemplateCategory.Naval]["LHAGroupGenerator"] = LHAGroupGenerator
# generators[TemplateCategory.Armor][
# "RandomArmorGroup"
# ] = FixedSizeArmorGroupGenerator
# generators[TemplateCategory.Armor][
# "RandomArmorGroupWithAA"
# ] = FixedSizeArmorGroupGeneratorWithAA
#
# generator_names = []
#
# for category, template_generators in generators.items():
# for generator_name, generator_class in template_generators.items():
# # Just reuse SamGroundObject to make it easy
# ground_object = SamGroundObject(
# namegen.random_objective_name(),
# initial_position,
# control_point,
# )
#
# if category in (
# TemplateCategory.Naval,
# TemplateCategory.Missile,
# TemplateCategory.Coastal,
# ):
# generator = generator_class(game, ground_object, game.blue.faction)
# elif category == TemplateCategory.Armor:
# unit_type = next(
# GroundUnitType.for_dcs_type(dcs.vehicles.Armor.M_1_Abrams)
# )
# generator = generator_class(
# game,
# ground_object,
# unit_type,
# # Create a group of 8 Armored Vehicles
# 8,
# )
# else:
# generator = generator_class(game, ground_object)
#
# # Generate the DCS Groups
# generator.generate()
#
# if isinstance(generator, EwrGenerator):
# template = AirDefenceTemplate("Early-Warning Radar", "EWR")
# elif isinstance(generator, AirDefenseGroupGenerator):
# template = AirDefenceTemplate(generator.name, generator.range().name)
# elif generator_name in MISSILES_MAP:
# template = MissileTemplate(generator_name, category.name)
# elif generator_name in COASTAL_MAP:
# template = CoastalTemplate(generator_name, category.name)
# elif category == TemplateCategory.Naval:
# if generator_name == "CarrierGroupGenerator":
# template = NavalTemplate("Carrier Group", "carrier")
# elif generator_name == "CarrierStrikeGroup8Generator":
# template = NavalTemplate("Carrier Strike Group 8", "carrier")
# elif generator_name == "LHAGroupGenerator":
# template = NavalTemplate("LHA Group", "lha")
# else:
# template = NavalTemplate(generator_name, "ship")
# elif category == TemplateCategory.Armor:
# if generator_name == "RandomArmorGroup":
# template = ArmorTemplate("Armor Group", "armor")
# elif generator_name == "RandomArmorGroupWithAA":
# template = ArmorTemplate("Armor Group with Anti-Air", "armor_aa")
# else:
# raise RuntimeError("Generator handling missing")
#
# groups = list(generator.groups)
#
# # These have to be identical!
# for i, group in enumerate(groups):
# for j, unit in enumerate(group.units):
# unit.name = f"{template.name} {i}-{j}"
#
# group_template = GroupTemplate(
# f"{template.name} #{str(i)}",
# [UnitTemplate.from_unit(unit) for unit in group.units],
# )
#
# if generator_name in [
# "CarrierGroupGenerator",
# "CarrierStrikeGroup8Generator",
# ]:
# if i == 0:
# group_template.randomizer = TemplateRandomizer(
# [0], 1, ["aircraft_carrier"]
# )
# elif i == 1:
# count = (
# 5 if generator_name == "CarrierStrikeGroup8Generator" else 4
# )
# group_template.randomizer = TemplateRandomizer(
# [], count, ["destroyers"]
# )
# elif generator_name == "LHAGroupGenerator":
# if i == 0:
# group_template.randomizer = TemplateRandomizer(
# [0], 1, ["helicopter_carrier"]
# )
# elif i == 1:
# group_template.randomizer = TemplateRandomizer(
# [], 2, ["destroyers"]
# )
# elif generator_name == "RandomArmorGroup" and i == 0:
# group_template.randomizer = TemplateRandomizer(
# [],
# [2, 6],
# ["frontline_units"],
# ["APC", "ATGM", "IFV", "Tank"],
# )
# elif generator_name == "RandomArmorGroupWithAA":
# if i == 0:
# group_template.randomizer = TemplateRandomizer(
# [],
# [2, 6],
# ["frontline_units"],
# ["APC", "ATGM", "IFV", "Tank"],
# )
# elif i == 1:
# group_template.randomizer = TemplateRandomizer(
# [],
# [1, 2],
# ["frontline_units", "infantry_units"],
# ["SHORADS", "MANPADS"],
# )
# group_template.optional = True
# elif generator_name == "EWRGenerator" and i == 0:
# for ewr_generator_name, ewr_generator in EWR_MAP.items():
# unit_type = next(
# GroundUnitType.for_dcs_type(ewr_generator.unit_type)
# )
# # Update all factions from generator to unit_type
# generator_names.append(
# [ewr_generator.unit_type.name, str(unit_type)]
# )
# # Update old generator names
# generator_names.append([ewr_generator_name, str(unit_type)])
#
# group_template.randomizer = TemplateRandomizer(
# [],
# 1,
# ["ewrs"], # Randomization based on faction template
# )
#
# template.groups.append(group_template)
#
# templates.add_template(category, template)
# generator_names.append([generator_name, template.name])
#
# # Load the basic templates
# temp_mis = dcs.Mission()
# temp_mis.load_file(input_miz)
#
# position_for_template: dict[str, Point] = {}
# group_for_template: dict[str, GroupTemplate] = {}
# for static_group in (
# temp_mis.country("USA").static_group
# + temp_mis.country("USAF Aggressors").static_group
# ):
# # Naming is: fob1 #001 -> name: fob1, category fob, group_name: fob1 #001
# template_name = str(static_group.name).split()[0]
# category_name, idx = template_name[:-1], int(template_name[-1])
#
# template = templates.by_name(template_name)
# if not template:
# template = BuildingTemplate(template_name, category_name)
#
# # Store original position to make the template relative to TGO later
# position_for_template[template_name] = static_group.position
#
# # Create Group Template for the satic group. Within Liberation we map
# # static units in groups even if dcs can not handle this. The dcs specific
# # handling will happpen later in miz generation again.
# group_template = GroupTemplate(f"{template.name}", [], True)
# group_for_template[template_name] = group_template
# template.groups.append(group_template)
#
# templates.add_template(TemplateCategory.Building, template)
#
# for u_id, unit in enumerate(static_group.units):
# unit_template = UnitTemplate.from_unit(unit)
# unit_template.position = Point(
# int(unit_template.position.x - position_for_template[template_name].x),
# int(unit_template.position.y - position_for_template[template_name].y),
# )
# group_for_template[template_name].units.append(unit_template)
#
# # Dump the template_map as json
# with open(templates_file, "w") as f:
# json.dump(templates.to_json(), f, cls=TemplateEncoder, indent=4)
#
# # Update Faction files
# update_factions(generator_names)
#
print(
"Migrate successful. To finish the migration also run export followed by an import again."
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
my_group = parser.add_mutually_exclusive_group(required=True)
my_group.add_argument(
"-i",
"--import",
dest="Import",
help="Imports the templates from the miz into liberation",
action="store_true",
)
my_group.add_argument(
"-e",
"--export",
dest="Export",
help="Exports the templates from liberation to the miz",
action="store_true",
)
my_group.add_argument(
"-m",
"--migrate",
dest="Migrate",
help="Migrates generators and the current ground object templates to the new system",
action="store_true",
)
my_group.add_argument(
"-t",
"--table",
dest="Table",
help="Prints a table of all templates",
action="store_true",
)
parser.add_argument(
"--template_miz", default=TEMPLATES_MIZ, help="Set the template mission (.miz)"
)
parser.add_argument(
"--target_file", default=TEMPLATES_BLOB, help="Set the target file"
)
parser.add_argument(
"--template_map", default=TEMPLATES_MAP, help="Set the template map"
)
parser.add_argument(
"--migrate_file", default=MIGRATE_MIZ, help="Set the migrate mission"
)
parser.add_argument(
"--table_file",
default=TABLE_FILE,
help="Output file for template documentation",
)
return parser.parse_args()
def main():
args = parse_args()
liberation_install.init()
miz_file = args.template_miz
target_file = args.target_file
migrate_file = args.migrate_file
template_map = args.template_map
table_file = args.table_file
if args.Import:
import_templates(miz_file, template_map, target_file, table_file)
elif args.Export:
export_templates(target_file, miz_file, template_map)
elif args.Table:
print_table(target_file, table_file)
elif args.Migrate:
migrate_generators_to_templates(
migrate_file, target_file, miz_file, template_map, table_file
)
if __name__ == "__main__":
main()