diff --git a/dcs/action.py b/dcs/action.py index 337096ad..fadeb2c4 100644 --- a/dcs/action.py +++ b/dcs/action.py @@ -1,7 +1,15 @@ -from typing import Any, Dict, List, Type, Optional +from __future__ import annotations + +from typing import Any, Dict, List, Type, Optional, TYPE_CHECKING from dcs.lua.serialize import dumps from dcs.translation import String, ResourceKey from enum import Enum, IntEnum +import dcs.countries as countries + +if TYPE_CHECKING: + from .mission import Mission + from .country import Country + from .unitgroup import Group class Action: @@ -1787,6 +1795,200 @@ def dict(self): return d +class PictureAction(Action): + + class HorzAlignment(Enum): + Left = "0" + Center = "1" + Right = "2" + + def __eq__(self, other: Any) -> bool: + if isinstance(other, str): + return self.value == other + return self == other + + class VertAlignment(Enum): + Top = "0" + Center = "1" + Bottom = "2" + + def __eq__(self, other: Any) -> bool: + if isinstance(other, str): + return self.value == other + return self == other + + class SizeUnits(Enum): + OriginalSize = "0" + WindowSize = "1" + + def __eq__(self, other: Any) -> bool: + if isinstance(other, str): + return self.value == other + return self == other + + def __init__(self, predicate: str, file_res_key: ResourceKey, seconds: int, + clearview: bool, start_delay: int, + horz_alignment: HorzAlignment, vert_alignment: VertAlignment, + size: int, size_units: SizeUnits) -> None: + super().__init__(predicate) + self.file_res_key = file_res_key + self.seconds = seconds + self.clearview = clearview + self.start_delay = start_delay + self.horz_alignment = horz_alignment + self.vert_alignment = vert_alignment + self.size_units = size_units + self.size = size + if size not in range(1, 101): + raise ValueError + + def dict(self) -> Dict[str, Any]: + d = super().dict() + d["file"] = self.file_res_key.key + d["seconds"] = self.seconds + d["clearview"] = self.clearview + d["start_delay"] = self.start_delay + d["horzAlignment"] = self.horz_alignment + d["vertAlignment"] = self.vert_alignment + d["size_units"] = self.size_units + d["size"] = self.size + return d + + def __eq__(self, other: Any) -> bool: + if isinstance(other, self.__class__): + return self.__dict__ == other.__dict__ + return False + + +class PictureToAll(PictureAction): + predicate = "a_out_picture" + + def __init__(self, file_res_key: ResourceKey, seconds: int, + clearview: bool, start_delay: int, + horz_alignment: PictureAction.HorzAlignment, vert_alignment: PictureAction.VertAlignment, + size: int, size_units: PictureAction.SizeUnits) -> None: + super().__init__(predicate=self.predicate, file_res_key=file_res_key, + seconds=seconds, clearview=clearview, + start_delay=start_delay, horz_alignment=horz_alignment, + vert_alignment=vert_alignment, size=size, size_units=size_units) + self.params = [self.file_res_key, seconds, clearview, start_delay, + horz_alignment, vert_alignment, size, size_units] + + @classmethod + def create_from_dict(cls, d: Dict[str, Any], mission: Mission) -> PictureToAll: + return cls(ResourceKey(d["file"]), d["seconds"], d["clearview"], d["start_delay"], + d["horzAlignment"], d["vertAlignment"], d["size"], d["size_units"]) + + +class PictureToCoalition(PictureAction): + predicate = "a_out_picture_s" + + def __init__(self, coalition: Coalition, file_res_key: ResourceKey, seconds: int, + clearview: bool, start_delay: int, + horz_alignment: PictureAction.HorzAlignment, vert_alignment: PictureAction.VertAlignment, + size: int, size_units: PictureAction.SizeUnits) -> None: + super().__init__(predicate=self.predicate, file_res_key=file_res_key, + seconds=seconds, clearview=clearview, + start_delay=start_delay, horz_alignment=horz_alignment, + vert_alignment=vert_alignment, size=size, size_units=size_units) + self.coalition = coalition + self.params = [self.coalition, self.file_res_key, seconds, clearview, + start_delay, horz_alignment, vert_alignment, size, size_units] + + @classmethod + def create_from_dict(cls, d: Dict[str, Any], mission: Mission) -> PictureToCoalition: + return cls(d["coalitionlist"], ResourceKey(d["file"]), d["seconds"], d["clearview"], d["start_delay"], + d["horzAlignment"], d["vertAlignment"], d["size"], d["size_units"]) + + def dict(self) -> Dict[str, Any]: + d = super().dict() + d["coalitionlist"] = self.coalition + return d + + +class PictureToCountry(PictureAction): + predicate = "a_out_picture_c" + + def __init__(self, country: Country, file_res_key: ResourceKey, seconds: int, + clearview: bool, start_delay: int, + horz_alignment: PictureAction.HorzAlignment, vert_alignment: PictureAction.VertAlignment, + size: int, size_units: PictureAction.SizeUnits) -> None: + super().__init__(predicate=self.predicate, file_res_key=file_res_key, + seconds=seconds, clearview=clearview, + start_delay=start_delay, horz_alignment=horz_alignment, + vert_alignment=vert_alignment, size=size, size_units=size_units) + self.country = country + self.params = [self.country.id, self.file_res_key, seconds, clearview, + start_delay, horz_alignment, vert_alignment, size, size_units] + + @classmethod + def create_from_dict(cls, d: Dict[str, Any], mission: Mission) -> PictureToCountry: + return cls(countries.get_by_id(d["countrylist"]), ResourceKey(d["file"]), + d["seconds"], d["clearview"], d["start_delay"], + d["horzAlignment"], d["vertAlignment"], d["size"], d["size_units"]) + + def dict(self) -> Dict[str, Any]: + d = super().dict() + d["countrylist"] = self.country.id + return d + + +class PictureToGroup(PictureAction): + predicate = "a_out_picture_g" + + def __init__(self, group: Group, file_res_key: ResourceKey, seconds: int, + clearview: bool, start_delay: int, + horz_alignment: PictureAction.HorzAlignment, vert_alignment: PictureAction.VertAlignment, + size: int, size_units: PictureAction.SizeUnits) -> None: + super().__init__(predicate=self.predicate, file_res_key=file_res_key, + seconds=seconds, clearview=clearview, + start_delay=start_delay, horz_alignment=horz_alignment, + vert_alignment=vert_alignment, size=size, size_units=size_units) + self.group = group + self.params = [self.group.id, self.file_res_key, seconds, clearview, + start_delay, horz_alignment, vert_alignment, size, size_units] + + @classmethod + def create_from_dict(cls, d: Dict[str, Any], mission: Mission) -> PictureToGroup: + group = mission.find_group_by_id(d["group"]) + if group is None: + raise RuntimeError("Group id {} found in PictureToGroup action, " + "but it does not exist in the mission.".format(d["group"])) + return cls(group, ResourceKey(d["file"]), d["seconds"], d["clearview"], d["start_delay"], + d["horzAlignment"], d["vertAlignment"], d["size"], d["size_units"]) + + def dict(self) -> Dict[str, Any]: + d = super().dict() + d["group"] = self.group.id + return d + + +class PictureToUnit(PictureAction): + predicate = "a_out_picture_u" + + def __init__(self, unit_id: int, file_res_key: ResourceKey, seconds: int, + clearview: bool, start_delay: int, + horz_alignment: PictureAction.HorzAlignment, vert_alignment: PictureAction.VertAlignment, + size: int, size_units: PictureAction.SizeUnits) -> None: + super().__init__(predicate=self.predicate, file_res_key=file_res_key, + seconds=seconds, clearview=clearview, + start_delay=start_delay, horz_alignment=horz_alignment, + vert_alignment=vert_alignment, size=size, size_units=size_units) + self.unit_id = unit_id + self.params = [self.unit_id, self.file_res_key, seconds, clearview, + start_delay, horz_alignment, vert_alignment, size, size_units] + + @classmethod + def create_from_dict(cls, d: Dict[str, Any], mission: Mission) -> PictureToUnit: + return cls(d["unit"], ResourceKey(d["file"]), d["seconds"], d["clearview"], d["start_delay"], + d["horzAlignment"], d["vertAlignment"], d["size"], d["size_units"]) + + def dict(self) -> Dict[str, Any]: + d = super().dict() + d["unit"] = self.unit_id + return d + + actions_map: Dict[str, Type[Action]] = { "a_activate_group": ActivateGroup, "a_add_radio_item": AddRadioItem, @@ -1865,5 +2067,10 @@ def dict(self): "a_set_ai_task": AITaskSet, "a_remove_scene_objects": RemoveSceneObjects, "a_scenery_destruction_zone": SceneryDestructionZone, - "a_zone_increment_resize": ZoneIncrementResize + "a_zone_increment_resize": ZoneIncrementResize, + "a_out_picture": PictureToAll, + "a_out_picture_s": PictureToCoalition, + "a_out_picture_c": PictureToCountry, + "a_out_picture_g": PictureToGroup, + "a_out_picture_u": PictureToUnit, } diff --git a/dcs/coalition.py b/dcs/coalition.py index 3f7660f5..f5f541a3 100644 --- a/dcs/coalition.py +++ b/dcs/coalition.py @@ -1,5 +1,5 @@ import sys -from typing import Dict, Union, List, TYPE_CHECKING +from typing import Dict, Union, List, TYPE_CHECKING, Optional import dcs.countries as countries from dcs.mapping import Point import dcs.unitgroup as unitgroup @@ -11,6 +11,7 @@ from dcs.point import MovingPoint, StaticPoint from dcs.country import Country from dcs.status_message import StatusMessage, MessageType, MessageSeverity +from dcs.unitgroup import Group if TYPE_CHECKING: from . import Mission @@ -289,6 +290,14 @@ def find_group(self, group_name, search="exact"): return None + def find_group_by_id(self, group_id: int) -> Optional[Group]: + for c in self.countries: + g = self.countries[c].find_group_by_id(group_id) + if g is not None: + return g + + return None + def dict(self): d = {"name": self.name} if self.bullseye: diff --git a/dcs/country.py b/dcs/country.py index 9f380805..85c6a560 100644 --- a/dcs/country.py +++ b/dcs/country.py @@ -3,7 +3,7 @@ from dcs.helicopters import HelicopterType from dcs.planes import PlaneType from dcs.unitgroup import VehicleGroup, ShipGroup, PlaneGroup, StaticGroup, HelicopterGroup, FlyingGroup, Group -from typing import List, Dict, Set, Type +from typing import List, Dict, Optional, Set, Type, Sequence def find_exact(group_name, find_name): @@ -81,6 +81,19 @@ def find_group(self, group_name, search="exact"): return group return None + def find_group_by_id(self, group_id: int) -> Optional[Group]: + groups: List[Sequence[Group]] = [self.vehicle_group, + self.ship_group, + self.plane_group, + self.helicopter_group, + self.static_group] + for search_group in groups: + for group in search_group: + if group.id == group_id: + return group + + return None + def find_vehicle_group(self, name: str, search="exact"): for group in self.vehicle_group: if find_map[search](group.name, name): diff --git a/dcs/mission.py b/dcs/mission.py index 650174d3..448b2d42 100644 --- a/dcs/mission.py +++ b/dcs/mission.py @@ -42,6 +42,7 @@ from dcs.helicopters import HelicopterType from dcs.planes import PlaneType from dcs.status_message import StatusMessage, MessageType, MessageSeverity +from dcs.unitgroup import Group class StartType(Enum): @@ -347,6 +348,12 @@ def loaddict(fname: str, mizfile: zipfile.ZipFile, reserved_files: List[str]) -> self.init_script_file = imp_mission.get("initScriptFile") self.init_script = imp_mission.get("initScript") + # import coalition with countries and units + for col_name in ["blue", "red", "neutrals"]: + if col_name in imp_mission["coalition"]: + self.coalition[col_name] = Coalition(col_name, imp_mission["coalition"][col_name]["bullseye"]) + status += self.coalition[col_name].load_from_dict(self, imp_mission["coalition"][col_name]) + # triggers self.bypassed_triggers = None self.bypassed_trigrules = None @@ -377,12 +384,6 @@ def loaddict(fname: str, mizfile: zipfile.ZipFile, reserved_files: List[str]) -> self.weather = weather.Weather(self.terrain) self.weather.load_from_dict(imp_weather) - # import coalition with countries and units - for col_name in ["blue", "red", "neutrals"]: - if col_name in imp_mission["coalition"]: - self.coalition[col_name] = Coalition(col_name, imp_mission["coalition"][col_name]["bullseye"]) - status += self.coalition[col_name].load_from_dict(self, imp_mission["coalition"][col_name]) - return status def sortie_text(self) -> str: @@ -1808,6 +1809,21 @@ def find_group(self, group_name, search="exact") -> Optional[unitgroup.Group]: return g return None + def find_group_by_id(self, group_id: int) -> Optional[Group]: + """Searches a group with the given groupId + + Args: + group_id: group identifier assigned by the mission file + + Returns: + Group: the group found, otherwise None + """ + for k in self.coalition: + g = self.coalition[k].find_group_by_id(group_id) + if g is not None: + return g + return None + def is_red(self, country: Country) -> bool: """Checks if the given country object is part o the red coalition. diff --git a/dcs/translation.py b/dcs/translation.py index fb028876..85dc2d7c 100644 --- a/dcs/translation.py +++ b/dcs/translation.py @@ -34,6 +34,11 @@ def key(self) -> str: def __str__(self): return self.res_key + def __eq__(self, other): + if isinstance(other, ResourceKey): + return self.__dict__ == other.__dict__ + return False + class Translation: def __init__(self, _mission): diff --git a/tests/missions/a_out_picture.miz b/tests/missions/a_out_picture.miz new file mode 100644 index 00000000..ea0f89b4 Binary files /dev/null and b/tests/missions/a_out_picture.miz differ diff --git a/tests/test_mission.py b/tests/test_mission.py index 7c4cd0b6..3e3c395c 100644 --- a/tests/test_mission.py +++ b/tests/test_mission.py @@ -12,6 +12,8 @@ from dcs.flyingunit import FlyingUnit from dcs.unit import Ship from dcs.task import WWIIFollowBigFormation +from dcs.action import PictureAction +from dcs.action import PictureToAll, PictureToCoalition, PictureToCountry, PictureToGroup, PictureToUnit class BasicTests(unittest.TestCase): @@ -1001,3 +1003,237 @@ def test_big_formation_action_right(self) -> None: m2_task = m.coalition['blue'].country("Combined Joint Task Forces Blue").plane_group[3].points[0].tasks[5] self.assertEqual(task, m2_task) + + def test_big_formation_action_leader(self) -> None: + m_name = "tests/missions/big-formation.miz" + m = dcs.mission.Mission() + m.load_file(m_name) + + assert isinstance(m.coalition['blue'].country("Combined Joint Task Forces Blue") + .plane_group[0].points[0].tasks[5], WWIIFollowBigFormation) + + task = m.coalition['blue'].country("Combined Joint Task Forces Blue").plane_group[0].points[0].tasks[5] + + self.assertNotIn("groupId", task.params) + self.assertNotIn("lastWptIndex", task.params) + self.assertEqual(task.params["formationType"], WWIIFollowBigFormation.FormationType.COMBAT_BOX_FOR_OPEN_FORMATION) + self.assertEqual(task.params["pos"], {"x": 0, "y": 0, "z": 0}) + self.assertTrue(task.params["lastWptIndexFlagChangedManually"]) + self.assertEqual(len(task.params), 7) + + m2_name = "missions/saved_big-formation.miz" + m.save(m2_name) + + m2 = dcs.mission.Mission() + m2.load_file(m2_name) + + assert isinstance(m2.coalition['blue'].country("Combined Joint Task Forces Blue") + .plane_group[0].points[0].tasks[5], WWIIFollowBigFormation) + m2_task = m.coalition['blue'].country("Combined Joint Task Forces Blue").plane_group[0].points[0].tasks[5] + + self.assertEqual(task, m2_task) + + def test_big_formation_action_left(self) -> None: + m_name = "tests/missions/big-formation.miz" + m = dcs.mission.Mission() + m.load_file(m_name) + + assert isinstance(m.coalition['blue'].country("Combined Joint Task Forces Blue") + .plane_group[1].points[0].tasks[5], WWIIFollowBigFormation) + task = m.coalition['blue'].country("Combined Joint Task Forces Blue").plane_group[1].points[0].tasks[5] + + self.assertEqual(task.params["formationType"], WWIIFollowBigFormation.FormationType.JAVELIN_DOWN) + self.assertEqual(task.params["pos"], {"x": -480, "y": -70, "z": -240}) + self.assertEqual(task.params["groupId"], 2) + self.assertEqual(task.params["posInGroup"], 2) + self.assertEqual(task.params["lastWptIndex"], 3) + self.assertTrue(task.params["lastWptIndexFlag"]) + self.assertEqual(len(task.params), 9) + + m2_name = "missions/saved_big-formation.miz" + m.save(m2_name) + + m2 = dcs.mission.Mission() + m2.load_file(m2_name) + + assert isinstance(m2.coalition['blue'].country("Combined Joint Task Forces Blue") + .plane_group[1].points[0].tasks[5], WWIIFollowBigFormation) + m2_task = m.coalition['blue'].country("Combined Joint Task Forces Blue").plane_group[1].points[0].tasks[5] + + self.assertEqual(task, m2_task) + + def test_big_formation_action_back(self) -> None: + m_name = "tests/missions/big-formation.miz" + m = dcs.mission.Mission() + m.load_file(m_name) + + assert isinstance(m.coalition['blue'].country("Combined Joint Task Forces Blue") + .plane_group[2].points[0].tasks[5], WWIIFollowBigFormation) + task = m.coalition['blue'].country("Combined Joint Task Forces Blue").plane_group[2].points[0].tasks[5] + + self.assertEqual(task.params["formationType"], WWIIFollowBigFormation.FormationType.COMBAT_BOX) + self.assertEqual(task.params["pos"], {"x": -320, "y": -50, "z": -0}) + self.assertEqual(task.params["groupId"], 2) + self.assertEqual(task.params["posInBox"], 3) + self.assertEqual(task.params["lastWptIndex"], 3) + self.assertFalse(task.params["lastWptIndexFlag"]) + self.assertEqual(len(task.params), 9) + + m2_name = "missions/saved_big-formation.miz" + m.save(m2_name) + + m2 = dcs.mission.Mission() + m2.load_file(m2_name) + + assert isinstance(m2.coalition['blue'].country("Combined Joint Task Forces Blue") + .plane_group[2].points[0].tasks[5], WWIIFollowBigFormation) + m2_task = m.coalition['blue'].country("Combined Joint Task Forces Blue").plane_group[2].points[0].tasks[5] + + self.assertEqual(task, m2_task) + + def test_big_formation_action_right(self) -> None: + m_name = "tests/missions/big-formation.miz" + m = dcs.mission.Mission() + m.load_file(m_name) + + assert isinstance(m.coalition['blue'].country("Combined Joint Task Forces Blue") + .plane_group[3].points[0].tasks[5], WWIIFollowBigFormation) + task = m.coalition['blue'].country("Combined Joint Task Forces Blue").plane_group[3].points[0].tasks[5] + + self.assertEqual(task.params["formationType"], WWIIFollowBigFormation.FormationType.COMBAT_BOX_FOR_OPEN_FORMATION) + self.assertEqual(task.params["pos"], {"x": -160, "y": 50, "z": 240}) + self.assertEqual(task.params["groupId"], 2) + self.assertEqual(task.params["posInBox"], 1) + self.assertEqual(task.params["lastWptIndex"], 3) + self.assertTrue(task.params["lastWptIndexFlag"]) + self.assertEqual(len(task.params), 9) + + m2_name = "missions/saved_big-formation.miz" + m.save(m2_name) + + m2 = dcs.mission.Mission() + m2.load_file(m2_name) + + assert isinstance(m2.coalition['blue'].country("Combined Joint Task Forces Blue") + .plane_group[3].points[0].tasks[5], WWIIFollowBigFormation) + m2_task = m.coalition['blue'].country("Combined Joint Task Forces Blue").plane_group[3].points[0].tasks[5] + + self.assertEqual(task, m2_task) + + def test_action_a_out_picture(self) -> None: + mizname = "tests/missions/a_out_picture.miz" + m = dcs.mission.Mission() + m.load_file(mizname) + + assert isinstance(m.triggerrules.triggers[0].actions[0], PictureToAll) + m_action = m.triggerrules.triggers[0].actions[0] + + self.assertEqual(m_action.seconds, 10) + self.assertFalse(m_action.clearview) + self.assertEqual(m_action.start_delay, 3) + self.assertEqual(m_action.horz_alignment, PictureAction.HorzAlignment.Left) + self.assertEqual(m_action.vert_alignment, PictureAction.VertAlignment.Top) + self.assertEqual(m_action.size, 100) + self.assertEqual(m_action.size_units, PictureAction.SizeUnits.OriginalSize) + + m2_name = "missions/saved_a_out_picture.miz" + m.save(m2_name) + + m2 = dcs.mission.Mission() + m2.load_file(m2_name) + + self.assertEqual(m_action, m2.triggerrules.triggers[0].actions[0]) + + def test_action_a_out_picture_s(self) -> None: + mizname = "tests/missions/a_out_picture.miz" + m = dcs.mission.Mission() + m.load_file(mizname) + + assert isinstance(m.triggerrules.triggers[0].actions[1], PictureToCoalition) + m_action = m.triggerrules.triggers[0].actions[1] + + self.assertEqual(m_action.coalition, "blue") + + m2_name = "missions/saved_a_out_picture.miz" + m.save(m2_name) + + m2 = dcs.mission.Mission() + m2.load_file(m2_name) + + self.assertEqual(m_action, m2.triggerrules.triggers[0].actions[1]) + + def test_action_a_out_picture_c(self) -> None: + mizname = "tests/missions/a_out_picture.miz" + m = dcs.mission.Mission() + m.load_file(mizname) + + assert isinstance(m.triggerrules.triggers[0].actions[2], PictureToCountry) + m_action = m.triggerrules.triggers[0].actions[2] + + self.assertEqual(m_action.country, dcs.countries.get_by_name("Ukraine")) + + m2_name = "missions/saved_a_out_picture.miz" + m.save(m2_name) + + m2 = dcs.mission.Mission() + m2.load_file(m2_name) + + self.assertEqual(m_action, m2.triggerrules.triggers[0].actions[2]) + + def test_action_a_out_picture_g(self) -> None: + mizname = "tests/missions/a_out_picture.miz" + m = dcs.mission.Mission() + m.load_file(mizname) + + assert isinstance(m.triggerrules.triggers[0].actions[3], PictureToGroup) + m_action = m.triggerrules.triggers[0].actions[3] + + self.assertEqual(m_action.group.id, 1) + + m2_name = "missions/saved_a_out_picture.miz" + m.save(m2_name) + + m2 = dcs.mission.Mission() + m2.load_file(m2_name) + + assert isinstance(m2.triggerrules.triggers[0].actions[3], PictureToGroup) + m2_action = m2.triggerrules.triggers[0].actions[3] + + self.assertEqual(m_action.group.id, m2_action.group.id) + + def test_action_a_out_picture_u_no_file(self) -> None: + mizname = "tests/missions/a_out_picture.miz" + m = dcs.mission.Mission() + m.load_file(mizname) + + assert isinstance(m.triggerrules.triggers[0].actions[4], PictureToUnit) + m_action = m.triggerrules.triggers[0].actions[4] + + self.assertEqual(m_action.unit_id, 1) + self.assertEqual(m_action.file_res_key.key, "") + + m2_name = "missions/saved_a_out_picture.miz" + m.save(m2_name) + + m2 = dcs.mission.Mission() + m2.load_file(m2_name) + + self.assertEqual(m_action, m2.triggerrules.triggers[0].actions[4]) + + def test_action_a_out_picture_u(self) -> None: + mizname = "tests/missions/a_out_picture.miz" + m = dcs.mission.Mission() + m.load_file(mizname) + + assert isinstance(m.triggerrules.triggers[0].actions[5], PictureToUnit) + m_action = m.triggerrules.triggers[0].actions[5] + + self.assertEqual(m_action.unit_id, 1) + + m2_name = "missions/saved_a_out_picture.miz" + m.save(m2_name) + + m2 = dcs.mission.Mission() + m2.load_file(m2_name) + + self.assertEqual(m_action, m2.triggerrules.triggers[0].actions[5])