Refactor luagenerator

- cleaned up the generation
- created special class to handle the serialization
- improved string escaping: Replace OS Path separator with normal slash and allow the usage of a single quote in unit names by changing the delimiter to double quote instead (1797)
- adjusted unit_name generation to prevent scripting errors with unescaped characters
This commit is contained in:
RndName 2021-06-27 17:46:39 +02:00
parent 8f16f242b1
commit 138e48dc2d
3 changed files with 224 additions and 197 deletions

View File

@ -19,6 +19,7 @@ from game.layout import LAYOUTS
from game.layout.layout import TgoLayout, TgoLayoutGroup from game.layout.layout import TgoLayout, TgoLayoutGroup
from game.point_with_heading import PointWithHeading from game.point_with_heading import PointWithHeading
from game.theater.theatergroup import TheaterGroup from game.theater.theatergroup import TheaterGroup
from game.utils import escape_string_for_lua
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@ -251,7 +252,10 @@ class ForceGroup:
# Assign UniqueID, name and align relative to ground_object # Assign UniqueID, name and align relative to ground_object
for unit in units: for unit in units:
unit.id = game.next_unit_id() 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( unit.position = PointWithHeading.from_point(
ground_object.position + unit.position, ground_object.position + unit.position,
# Align heading to GroundObject defined by the campaign designer # Align heading to GroundObject defined by the campaign designer

View File

@ -2,8 +2,9 @@ from __future__ import annotations
import logging import logging
import os import os
from abc import ABC, abstractmethod
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional
from dcs import Mission from dcs import Mission
from dcs.action import DoScript, DoScriptFile from dcs.action import DoScript, DoScriptFile
@ -13,6 +14,7 @@ from dcs.triggers import TriggerStart
from game.ato import FlightType from game.ato import FlightType
from game.plugins import LuaPluginManager from game.plugins import LuaPluginManager
from game.theater import TheaterGroundObject from game.theater import TheaterGroundObject
from game.utils import escape_string_for_lua
from .aircraft.flightdata import FlightData from .aircraft.flightdata import FlightData
from .airsupport import AirSupport from .airsupport import AirSupport
@ -40,43 +42,42 @@ class LuaGenerator:
self.inject_plugins() self.inject_plugins()
def generate_plugin_data(self) -> None: def generate_plugin_data(self) -> None:
# TODO: Refactor this lua_data = LuaData("dcsLiberation")
lua_data = {
"AircraftCarriers": {},
"Tankers": {},
"AWACs": {},
"JTACs": {},
"TargetPoints": {},
"RedAA": {},
"BlueAA": {},
} # type: ignore
for i, tanker in enumerate(self.air_support.tankers): install_path = lua_data.add_item("installPath")
lua_data["Tankers"][i] = { install_path.set_value(os.path.abspath("."))
"dcsGroupName": tanker.group_name,
"callsign": tanker.callsign,
"variant": tanker.variant,
"radio": tanker.freq.mhz,
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name,
}
for i, awacs in enumerate(self.air_support.awacs): lua_data.add_item("Airbases")
lua_data["AWACs"][i] = { lua_data.add_item("Carriers")
"dcsGroupName": awacs.group_name,
"callsign": awacs.callsign,
"radio": awacs.freq.mhz,
}
for i, jtac in enumerate(self.air_support.jtacs): tankers_object = lua_data.add_item("Tankers")
lua_data["JTACs"][i] = { for tanker in self.air_support.tankers:
"dcsGroupName": jtac.group_name, tanker_item = tankers_object.add_item()
"callsign": jtac.callsign, tanker_item.add_key_value("dcsGroupName", tanker.group_name)
"zone": jtac.region, tanker_item.add_key_value("callsign", tanker.callsign)
"dcsUnit": jtac.unit_name, tanker_item.add_key_value("variant", tanker.variant)
"laserCode": jtac.code, tanker_item.add_key_value("radio", str(tanker.freq.mhz))
"radio": jtac.freq.mhz, tanker_item.add_key_value(
} "tacan", str(tanker.tacan.number) + tanker.tacan.band.name
flight_count = 0 )
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: for flight in self.flights:
if flight.friendly and flight.flight_type in [ if flight.friendly and flight.flight_type in [
FlightType.ANTISHIP, FlightType.ANTISHIP,
@ -97,17 +98,24 @@ class LuaGenerator:
elif hasattr(flight_target, "name"): elif hasattr(flight_target, "name"):
flight_target_name = flight_target.name flight_target_name = flight_target.name
flight_target_type = flight_type + " TGT (Airbase)" flight_target_type = flight_type + " TGT (Airbase)"
lua_data["TargetPoints"][flight_count] = { target_item = target_points.add_item()
"name": flight_target_name, if flight_target_name:
"type": flight_target_type, target_item.add_key_value("name", flight_target_name)
"position": { if flight_target_type:
"x": flight_target.position.x, target_item.add_key_value("type", flight_target_type)
"y": flight_target.position.y, target_item.add_key_value(
}, "positionX", str(flight_target.position.x)
} )
flight_count += 1 target_item.add_key_value(
"positionY", str(flight_target.position.y)
)
for cp in self.game.theater.controlpoints: 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: for ground_object in cp.ground_objects:
if ground_object.might_have_aa and not ground_object.is_dead: if ground_object.might_have_aa and not ground_object.is_dead:
for g in ground_object.groups: for g in ground_object.groups:
@ -116,160 +124,18 @@ class LuaGenerator:
if not threat_range: if not threat_range:
continue continue
faction = "BlueAA" if cp.captured else "RedAA" aa_item = coalition_object.add_item()
aa_item.add_key_value("name", ground_object.name)
lua_data[faction][g.group_name] = { aa_item.add_key_value("range", str(threat_range.meters))
"name": ground_object.name, aa_item.add_key_value(
"range": threat_range.meters, "positionX", str(ground_object.position.x)
"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 aa_item.add_key_value(
lua += """ "positionY", str(ground_object.position.y)
-- 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 += """
"""
trigger = TriggerStart(comment="Set DCS Liberation data") 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) self.mission.triggerrules.triggers.append(trigger)
def inject_lua_trigger(self, contents: str, comment: str) -> None: def inject_lua_trigger(self, contents: str, comment: str) -> None:
@ -307,3 +173,152 @@ class LuaGenerator:
if plugin.enabled: if plugin.enabled:
plugin.inject_scripts(self) plugin.inject_scripts(self)
plugin.inject_configuration(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()

View File

@ -466,3 +466,11 @@ def interpolate(value1: float, value2: float, factor: float, clamp: bool) -> flo
def dcs_to_shapely_point(point: Point) -> ShapelyPoint: def dcs_to_shapely_point(point: Point) -> ShapelyPoint:
return ShapelyPoint(point.x, point.y) 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)