diff --git a/game/armedforces/forcegroup.py b/game/armedforces/forcegroup.py index 15c17685..21250f11 100644 --- a/game/armedforces/forcegroup.py +++ b/game/armedforces/forcegroup.py @@ -19,6 +19,7 @@ from game.layout import LAYOUTS from game.layout.layout import TgoLayout, TgoLayoutGroup from game.point_with_heading import PointWithHeading from game.theater.theatergroup import TheaterGroup +from game.utils import escape_string_for_lua if TYPE_CHECKING: from game import Game @@ -251,7 +252,10 @@ class ForceGroup: # Assign UniqueID, name and align relative to ground_object for unit in units: unit.id = game.next_unit_id() - unit.name = unit.unit_type.name if unit.unit_type else unit.type.name + # Add unit name escaped so that we do not have scripting issues later + unit.name = escape_string_for_lua( + unit.unit_type.name if unit.unit_type else unit.type.name + ) unit.position = PointWithHeading.from_point( ground_object.position + unit.position, # Align heading to GroundObject defined by the campaign designer diff --git a/game/missiongenerator/luagenerator.py b/game/missiongenerator/luagenerator.py index 5fd5a0d3..b0c3ade6 100644 --- a/game/missiongenerator/luagenerator.py +++ b/game/missiongenerator/luagenerator.py @@ -2,8 +2,9 @@ from __future__ import annotations import logging import os +from abc import ABC, abstractmethod from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from dcs import Mission from dcs.action import DoScript, DoScriptFile @@ -13,6 +14,7 @@ from dcs.triggers import TriggerStart from game.ato import FlightType from game.plugins import LuaPluginManager from game.theater import TheaterGroundObject +from game.utils import escape_string_for_lua from .aircraft.flightdata import FlightData from .airsupport import AirSupport @@ -40,43 +42,42 @@ class LuaGenerator: self.inject_plugins() def generate_plugin_data(self) -> None: - # TODO: Refactor this - lua_data = { - "AircraftCarriers": {}, - "Tankers": {}, - "AWACs": {}, - "JTACs": {}, - "TargetPoints": {}, - "RedAA": {}, - "BlueAA": {}, - } # type: ignore + lua_data = LuaData("dcsLiberation") - for i, tanker in enumerate(self.air_support.tankers): - lua_data["Tankers"][i] = { - "dcsGroupName": tanker.group_name, - "callsign": tanker.callsign, - "variant": tanker.variant, - "radio": tanker.freq.mhz, - "tacan": str(tanker.tacan.number) + tanker.tacan.band.name, - } + install_path = lua_data.add_item("installPath") + install_path.set_value(os.path.abspath(".")) - for i, awacs in enumerate(self.air_support.awacs): - lua_data["AWACs"][i] = { - "dcsGroupName": awacs.group_name, - "callsign": awacs.callsign, - "radio": awacs.freq.mhz, - } + lua_data.add_item("Airbases") + lua_data.add_item("Carriers") - for i, jtac in enumerate(self.air_support.jtacs): - lua_data["JTACs"][i] = { - "dcsGroupName": jtac.group_name, - "callsign": jtac.callsign, - "zone": jtac.region, - "dcsUnit": jtac.unit_name, - "laserCode": jtac.code, - "radio": jtac.freq.mhz, - } - flight_count = 0 + tankers_object = lua_data.add_item("Tankers") + for tanker in self.air_support.tankers: + tanker_item = tankers_object.add_item() + tanker_item.add_key_value("dcsGroupName", tanker.group_name) + tanker_item.add_key_value("callsign", tanker.callsign) + tanker_item.add_key_value("variant", tanker.variant) + tanker_item.add_key_value("radio", str(tanker.freq.mhz)) + tanker_item.add_key_value( + "tacan", str(tanker.tacan.number) + tanker.tacan.band.name + ) + + awacs_object = lua_data.add_item("AWACs") + for awacs in self.air_support.awacs: + awacs_item = awacs_object.add_item() + awacs_item.add_key_value("dcsGroupName", awacs.group_name) + awacs_item.add_key_value("callsign", awacs.callsign) + awacs_item.add_key_value("radio", str(awacs.freq.mhz)) + + jtacs_object = lua_data.add_item("JTACs") + for jtac in self.air_support.jtacs: + jtac_item = jtacs_object.add_item() + jtac_item.add_key_value("dcsGroupName", jtac.group_name) + jtac_item.add_key_value("callsign", jtac.callsign) + jtac_item.add_key_value("zone", jtac.region) + jtac_item.add_key_value("dcsUnit", jtac.unit_name) + jtac_item.add_key_value("laserCode", jtac.code) + + target_points = lua_data.add_item("TargetPoints") for flight in self.flights: if flight.friendly and flight.flight_type in [ FlightType.ANTISHIP, @@ -97,17 +98,24 @@ class LuaGenerator: elif hasattr(flight_target, "name"): flight_target_name = flight_target.name flight_target_type = flight_type + " TGT (Airbase)" - lua_data["TargetPoints"][flight_count] = { - "name": flight_target_name, - "type": flight_target_type, - "position": { - "x": flight_target.position.x, - "y": flight_target.position.y, - }, - } - flight_count += 1 + target_item = target_points.add_item() + if flight_target_name: + target_item.add_key_value("name", flight_target_name) + if flight_target_type: + target_item.add_key_value("type", flight_target_type) + target_item.add_key_value( + "positionX", str(flight_target.position.x) + ) + target_item.add_key_value( + "positionY", str(flight_target.position.y) + ) for cp in self.game.theater.controlpoints: + coalition_object = ( + lua_data.get_or_create_item("BlueAA") + if cp.captured + else lua_data.get_or_create_item("RedAA") + ) for ground_object in cp.ground_objects: if ground_object.might_have_aa and not ground_object.is_dead: for g in ground_object.groups: @@ -116,160 +124,18 @@ class LuaGenerator: if not threat_range: continue - faction = "BlueAA" if cp.captured else "RedAA" - - lua_data[faction][g.group_name] = { - "name": ground_object.name, - "range": threat_range.meters, - "position": { - "x": ground_object.position.x, - "y": ground_object.position.y, - }, - } - - # set a LUA table with data from Liberation that we want to set - # at the moment it contains Liberation's install path, and an overridable - # definition for the JTACAutoLase function later, we'll add data about the units - # and points having been generated, in order to facilitate the configuration of - # the plugin lua scripts - state_location = "[[" + os.path.abspath(".") + "]]" - lua = ( - """ - -- setting configuration table - env.info("DCSLiberation|: setting configuration table") - - -- all data in this table is overridable. - dcsLiberation = {} - - -- the base location for state.json; if non-existent, it'll be replaced with - -- LIBERATION_EXPORT_DIR, TEMP, or DCS working directory - dcsLiberation.installPath=""" - + state_location - + """ - - """ - ) - # Process the tankers - lua += """ - - -- list the tankers generated by Liberation - dcsLiberation.Tankers = { - """ - for key in lua_data["Tankers"]: - data = lua_data["Tankers"][key] - dcs_group_name = data["dcsGroupName"] - callsign = data["callsign"] - variant = data["variant"] - tacan = data["tacan"] - radio = data["radio"] - lua += ( - f" {{dcsGroupName='{dcs_group_name}', callsign='{callsign}', " - f"variant='{variant}', tacan='{tacan}', radio='{radio}' }}, \n" - ) - lua += "}" - - # Process the AWACSes - lua += """ - - -- list the AWACs generated by Liberation - dcsLiberation.AWACs = { - """ - for key in lua_data["AWACs"]: - data = lua_data["AWACs"][key] - dcs_group_name = data["dcsGroupName"] - callsign = data["callsign"] - radio = data["radio"] - lua += ( - f" {{dcsGroupName='{dcs_group_name}', callsign='{callsign}', " - f"radio='{radio}' }}, \n" - ) - lua += "}" - - # Process the JTACs - lua += """ - - -- list the JTACs generated by Liberation - dcsLiberation.JTACs = { - """ - for key in lua_data["JTACs"]: - data = lua_data["JTACs"][key] - dcs_group_name = data["dcsGroupName"] - callsign = data["callsign"] - zone = data["zone"] - laser_code = data["laserCode"] - dcs_unit = data["dcsUnit"] - radio = data["radio"] - lua += ( - f" {{dcsGroupName='{dcs_group_name}', callsign='{callsign}', " - f"zone={repr(zone)}, laserCode='{laser_code}', dcsUnit='{dcs_unit}', " - f"radio='{radio}' }}, \n" - ) - lua += "}" - - # Process the Target Points - lua += """ - - -- list the target points generated by Liberation - dcsLiberation.TargetPoints = { - """ - for key in lua_data["TargetPoints"]: - data = lua_data["TargetPoints"][key] - name = data["name"] - point_type = data["type"] - position_x = data["position"]["x"] - position_y = data["position"]["y"] - lua += ( - f" {{name='{name}', pointType='{point_type}', " - f"positionX='{position_x}', positionY='{position_y}' }}, \n" - ) - lua += "}" - - lua += """ - - -- list the airbases generated by Liberation - -- dcsLiberation.Airbases = {} - - -- list the aircraft carriers generated by Liberation - -- dcsLiberation.Carriers = {} - - -- list the Red AA generated by Liberation - dcsLiberation.RedAA = { - """ - for key in lua_data["RedAA"]: - data = lua_data["RedAA"][key] - name = data["name"] - radius = data["range"] - position_x = data["position"]["x"] - position_y = data["position"]["y"] - lua += ( - f" {{dcsGroupName='{key}', name='{name}', range='{radius}', " - f"positionX='{position_x}', positionY='{position_y}' }}, \n" - ) - lua += "}" - - lua += """ - - -- list the Blue AA generated by Liberation - dcsLiberation.BlueAA = { - """ - for key in lua_data["BlueAA"]: - data = lua_data["BlueAA"][key] - name = data["name"] - radius = data["range"] - position_x = data["position"]["x"] - position_y = data["position"]["y"] - lua += ( - f" {{dcsGroupName='{key}', name='{name}', range='{radius}', " - f"positionX='{position_x}', positionY='{position_y}' }}, \n" - ) - lua += "}" - - lua += """ - - """ + aa_item = coalition_object.add_item() + aa_item.add_key_value("name", ground_object.name) + aa_item.add_key_value("range", str(threat_range.meters)) + aa_item.add_key_value( + "positionX", str(ground_object.position.x) + ) + aa_item.add_key_value( + "positionY", str(ground_object.position.y) + ) trigger = TriggerStart(comment="Set DCS Liberation data") - trigger.add_action(DoScript(String(lua))) + trigger.add_action(DoScript(String(lua_data.create_operations_lua()))) self.mission.triggerrules.triggers.append(trigger) def inject_lua_trigger(self, contents: str, comment: str) -> None: @@ -307,3 +173,152 @@ class LuaGenerator: if plugin.enabled: plugin.inject_scripts(self) plugin.inject_configuration(self) + + +class LuaValue: + key: Optional[str] + value: str | list[str] + + def __init__(self, key: Optional[str], value: str | list[str]): + self.key = key + self.value = value + + def _escape_value(self, value: str) -> str: + value = value.replace('"', "'") # Replace Double Quote as this is the delimiter + value = value.replace(os.sep, "/") # Replace Backslash as path separator + return '"{0}"'.format(value) + + def serialize(self) -> str: + serialized_value = self.key + " = " if self.key else "" + if isinstance(self.value, str): + serialized_value += f'"{escape_string_for_lua(self.value)}"' + else: + escaped_values = [f'"{escape_string_for_lua(v)}"' for v in self.value] + serialized_value += "{" + ", ".join(escaped_values) + "}" + return serialized_value + + +class LuaItem(ABC): + value: LuaValue | list[LuaValue] + name: Optional[str] + + def __init__(self, name: Optional[str]): + self.value = [] + self.name = name + + def set_value(self, value: str) -> None: + self.value = LuaValue(None, value) + + def add_data_array(self, key: str, values: list[str]) -> None: + self._add_value(LuaValue(key, values)) + + def add_key_value(self, key: str, value: str) -> None: + self._add_value(LuaValue(key, value)) + + def _add_value(self, value: LuaValue) -> None: + if isinstance(self.value, list): + self.value.append(value) + else: + self.value = value + + @abstractmethod + def add_item(self, item_name: Optional[str] = None) -> LuaItem: + """adds a new item to the LuaArray without checking the existence""" + raise NotImplementedError + + @abstractmethod + def get_item(self, item_name: str) -> Optional[LuaItem]: + """gets item from LuaArray. Returns None if it does not exist""" + raise NotImplementedError + + @abstractmethod + def get_or_create_item(self, item_name: Optional[str] = None) -> LuaItem: + """gets item from the LuaArray or creates one if it does not exist already""" + raise NotImplementedError + + @abstractmethod + def serialize(self) -> str: + if isinstance(self.value, LuaValue): + return self.value.serialize() + else: + serialized_data = [d.serialize() for d in self.value] + return "{" + ", ".join(serialized_data) + "}" + + +class LuaData(LuaItem): + objects: list[LuaData] + base_name: Optional[str] + + def __init__(self, name: Optional[str], is_base_name: bool = True): + self.objects = [] + self.base_name = name if is_base_name else None + super().__init__(name) + + def add_item(self, item_name: Optional[str] = None) -> LuaItem: + """adds a new item to the LuaArray without checking the existence""" + item = LuaData(item_name, False) + self.objects.append(item) + return item + + def get_item(self, item_name: str) -> Optional[LuaItem]: + """gets item from LuaArray. Returns None if it does not exist""" + for lua_object in self.objects: + if lua_object.name == item_name: + return lua_object + return None + + def get_or_create_item(self, item_name: Optional[str] = None) -> LuaItem: + """gets item from the LuaArray or creates one if it does not exist already""" + if item_name: + item = self.get_item(item_name) + if item: + return item + return self.add_item(item_name) + + def serialize(self, level: int = 0) -> str: + """serialize the LuaData to a string""" + serialized_data: list[str] = [] + serialized_name = "" + linebreak = "\n" + tab = "\t" + tab_end = "" + for _ in range(level): + tab += "\t" + tab_end += "\t" + if self.base_name: + # Only used for initialization of the object in lua + serialized_name += self.base_name + " = " + if self.objects: + # nested objects + serialized_objects = [o.serialize(level + 1) for o in self.objects] + if self.name: + if self.name is not self.base_name: + serialized_name += self.name + " = " + serialized_data.append( + serialized_name + + "{" + + linebreak + + tab + + ("," + linebreak + tab).join(serialized_objects) + + linebreak + + tab_end + + "}" + ) + else: + # key with value + if self.name: + serialized_data.append(self.name + " = " + super().serialize()) + # only value + else: + serialized_data.append(super().serialize()) + + return "\n".join(serialized_data) + + def create_operations_lua(self) -> str: + """crates the liberation lua script for the dcs mission""" + lua_prefix = """ +-- setting configuration table +env.info("DCSLiberation|: setting configuration table") +""" + + return lua_prefix + self.serialize() diff --git a/game/utils.py b/game/utils.py index ad8f8fed..fcee838a 100644 --- a/game/utils.py +++ b/game/utils.py @@ -466,3 +466,11 @@ def interpolate(value1: float, value2: float, factor: float, clamp: bool) -> flo def dcs_to_shapely_point(point: Point) -> ShapelyPoint: return ShapelyPoint(point.x, point.y) + + +def escape_string_for_lua(value: str) -> str: + """Escapes special characters from a string. + This prevents scripting errors in lua scripts""" + value = value.replace('"', "'") # Replace Double Quote as this is the delimiter + value = value.replace(os.sep, "/") # Replace Backslash as path separator + return "{0}".format(value)