Merge branch 'develop' into faction_refactor

This commit is contained in:
Khopa 2020-10-22 13:40:14 +02:00
commit d6b94345d9
14 changed files with 354 additions and 139 deletions

View File

@ -358,7 +358,7 @@ class Operation:
# set a LUA table with data from Liberation that we want to set # 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 # 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 # 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("state.json") + "]]" state_location = "[[" + os.path.abspath(".") + "]]"
lua = """ lua = """
-- setting configuration table -- setting configuration table
env.info("DCSLiberation|: setting configuration table") env.info("DCSLiberation|: setting configuration table")

View File

@ -1,14 +1,14 @@
def meter_to_feet(value_in_meter): def meter_to_feet(value_in_meter: float) -> int:
return int(3.28084 * value_in_meter) return int(3.28084 * value_in_meter)
def feet_to_meter(value_in_feet): def feet_to_meter(value_in_feet: float) -> int:
return int(float(value_in_feet)/3.048) return int(value_in_feet / 3.28084)
def meter_to_nm(value_in_meter): def meter_to_nm(value_in_meter: float) -> int:
return int(float(value_in_meter)*0.000539957) return int(value_in_meter / 1852)
def nm_to_meter(value_in_nm): def nm_to_meter(value_in_nm: float) -> int:
return int(float(value_in_nm)*1852) return int(value_in_nm * 1852)

View File

@ -3,11 +3,11 @@ from __future__ import annotations
import logging import logging
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple, Type, Union from typing import Dict, List, Optional, Type, Union
from dcs import helicopters from dcs import helicopters
from dcs.action import AITaskPush, ActivateGroup, MessageToAll from dcs.action import AITaskPush, ActivateGroup
from dcs.condition import CoalitionHasAirdrome, PartOfCoalitionInZone, TimeAfter from dcs.condition import CoalitionHasAirdrome, TimeAfter
from dcs.country import Country from dcs.country import Country
from dcs.flyingunit import FlyingUnit from dcs.flyingunit import FlyingUnit
from dcs.helicopters import UH_1H, helicopter_map from dcs.helicopters import UH_1H, helicopter_map
@ -40,7 +40,6 @@ from dcs.task import (
ControlledTask, ControlledTask,
EPLRS, EPLRS,
EngageTargets, EngageTargets,
Escort,
GroundAttack, GroundAttack,
OptROE, OptROE,
OptRTBOnBingoFuel, OptRTBOnBingoFuel,
@ -55,10 +54,10 @@ from dcs.task import (
Targets, Targets,
Task, Task,
) )
from dcs.terrain.terrain import Airport, NoParkingSlotError from dcs.terrain.terrain import Airport
from dcs.translation import String from dcs.translation import String
from dcs.triggers import Event, TriggerOnce, TriggerRule from dcs.triggers import Event, TriggerOnce, TriggerRule
from dcs.unitgroup import FlyingGroup, Group, ShipGroup, StaticGroup from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
from dcs.unittype import FlyingType, UnitType from dcs.unittype import FlyingType, UnitType
from game import db from game import db
@ -548,7 +547,6 @@ class AircraftConflictGenerator:
self.settings = settings self.settings = settings
self.conflict = conflict self.conflict = conflict
self.radio_registry = radio_registry self.radio_registry = radio_registry
self.escort_targets: List[Tuple[FlyingGroup, int]] = []
self.flights: List[FlightData] = [] self.flights: List[FlightData] = []
def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency: def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency:
@ -633,10 +631,6 @@ class AircraftConflictGenerator:
logging.warning(f"Unhandled departure control point: {cp.cptype}") logging.warning(f"Unhandled departure control point: {cp.cptype}")
departure_runway = fallback_runway departure_runway = fallback_runway
# The first waypoint is set automatically by pydcs, so it's not in our
# list. Convert the pydcs MovingPoint to a FlightWaypoint so it shows up
# in our FlightData.
first_point = FlightWaypoint.from_pydcs(group.points[0], flight.from_cp)
self.flights.append(FlightData( self.flights.append(FlightData(
flight_type=flight.flight_type, flight_type=flight.flight_type,
units=group.units, units=group.units,
@ -808,8 +802,8 @@ class AircraftConflictGenerator:
logging.info(f"Generating flight: {flight.unit_type}") logging.info(f"Generating flight: {flight.unit_type}")
group = self.generate_planned_flight(flight.from_cp, country, group = self.generate_planned_flight(flight.from_cp, country,
flight) flight)
self.setup_flight_group(group, package, flight, timing, self.setup_flight_group(group, flight, dynamic_runways)
dynamic_runways) self.create_waypoints(group, package, flight, timing)
def set_activation_time(self, flight: Flight, group: FlyingGroup, def set_activation_time(self, flight: Flight, group: FlyingGroup,
delay: int) -> None: delay: int) -> None:
@ -988,8 +982,11 @@ class AircraftConflictGenerator:
def configure_escort(self, group: FlyingGroup, flight: Flight, def configure_escort(self, group: FlyingGroup, flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None: dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = Escort.name # Escort groups are actually given the CAP task so they can perform the
self._setup_group(group, Escort, flight, dynamic_runways) # Search Then Engage task, which we have to use instead of the Escort
# task for the reasons explained in JoinPointBuilder.
group.task = CAP.name
self._setup_group(group, CAP, flight, dynamic_runways)
self.configure_behavior(group, roe=OptROE.Values.OpenFire, self.configure_behavior(group, roe=OptROE.Values.OpenFire,
restrict_jettison=True) restrict_jettison=True)
@ -998,8 +995,7 @@ class AircraftConflictGenerator:
logging.error(f"Unhandled flight type: {flight.flight_type.name}") logging.error(f"Unhandled flight type: {flight.flight_type.name}")
self.configure_behavior(group) self.configure_behavior(group)
def setup_flight_group(self, group: FlyingGroup, package: Package, def setup_flight_group(self, group: FlyingGroup, flight: Flight,
flight: Flight, timing: PackageWaypointTiming,
dynamic_runways: Dict[str, RunwayData]) -> None: dynamic_runways: Dict[str, RunwayData]) -> None:
flight_type = flight.flight_type flight_type = flight.flight_type
if flight_type in [FlightType.BARCAP, FlightType.TARCAP, if flight_type in [FlightType.BARCAP, FlightType.TARCAP,
@ -1020,16 +1016,23 @@ class AircraftConflictGenerator:
self.configure_eplrs(group, flight) self.configure_eplrs(group, flight)
def create_waypoints(self, group: FlyingGroup, package: Package,
flight: Flight, timing: PackageWaypointTiming) -> None:
for waypoint in flight.points: for waypoint in flight.points:
waypoint.tot = None waypoint.tot = None
takeoff_point = FlightWaypoint.from_pydcs(group.points[0], takeoff_point = FlightWaypoint.from_pydcs(group.points[0],
flight.from_cp) flight.from_cp)
self.set_takeoff_time(takeoff_point, package, flight, group) self.set_takeoff_time(takeoff_point, package, flight, group)
filtered_points = []
for point in flight.points: for point in flight.points:
if point.only_for_player and not flight.client_count: if point.only_for_player and not flight.client_count:
continue continue
filtered_points.append(point)
for idx, point in enumerate(filtered_points):
PydcsWaypointBuilder.for_waypoint( PydcsWaypointBuilder.for_waypoint(
point, group, flight, timing, self.m point, group, flight, timing, self.m
).build() ).build()
@ -1114,6 +1117,8 @@ class PydcsWaypointBuilder:
mission: Mission) -> PydcsWaypointBuilder: mission: Mission) -> PydcsWaypointBuilder:
builders = { builders = {
FlightWaypointType.EGRESS: EgressPointBuilder, FlightWaypointType.EGRESS: EgressPointBuilder,
FlightWaypointType.INGRESS_CAS: IngressBuilder,
FlightWaypointType.INGRESS_ESCORT: IngressBuilder,
FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder, FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder,
FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder, FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder,
FlightWaypointType.JOIN: JoinPointBuilder, FlightWaypointType.JOIN: JoinPointBuilder,
@ -1236,8 +1241,48 @@ class JoinPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint: def build(self) -> MovingPoint:
waypoint = super().build() waypoint = super().build()
self.set_waypoint_tot(waypoint, self.timing.join) self.set_waypoint_tot(waypoint, self.timing.join)
if self.flight.flight_type == FlightType.ESCORT:
self.configure_escort_tasks(waypoint)
return waypoint return waypoint
@staticmethod
def configure_escort_tasks(waypoint: MovingPoint) -> None:
# Ideally we would use the escort mission type and escort task to have
# the AI automatically but the AI only escorts AI flights while they are
# traveling between waypoints. When an AI flight performs an attack
# (such as attacking the mission target), AI escorts wander aimlessly
# until the escorted group resumes its flight plan.
#
# As such, we instead use the Search Then Engage task, which is an
# enroute task that causes the AI to follow their flight plan and engage
# enemies of the set type within a certain distance. The downside to
# this approach is that AI escorts are no longer related to the group
# they are escorting, aside from the fact that they fly a similar flight
# plan at the same time. With Escort, the escorts will follow the
# escorted group out of the area. The strike element may or may not fly
# directly over the target, and they may or may not require multiple
# attack runs. For the escort flight we must just assume a flight plan
# for the escort to fly. If the strike flight doesn't need to overfly
# the target, the escorts are needlessly going in harms way. If the
# strike flight needs multiple passes, the escorts may leave before the
# escorted aircraft do.
#
# Another possible option would be to use Search Then Engage for join ->
# ingress and egress -> split, but use a Search Then Engage in Zone task
# for the target area that is set to end on a flag flip that occurs when
# the strike aircraft finish their attack task.
#
# https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/250183-task-follow-and-escort-temporarily-aborted
waypoint.add_task(ControlledTask(EngageTargets(
# TODO: From doctrine.
max_distance=nm_to_meter(30),
targets=[Targets.All.Air.Planes.Fighters]
)))
# We could set this task to end at the split point. pydcs doesn't
# currently support that task end condition though, and we don't really
# need it.
class LandingPointBuilder(PydcsWaypointBuilder): class LandingPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint: def build(self) -> MovingPoint:

View File

@ -52,6 +52,7 @@ class FlightWaypointType(Enum):
JOIN = 16 JOIN = 16
SPLIT = 17 SPLIT = 17
LOITER = 18 LOITER = 18
INGRESS_ESCORT = 19
class PredefinedWaypointCategory(Enum): class PredefinedWaypointCategory(Enum):

View File

@ -132,7 +132,7 @@ class FlightPlanBuilder:
if not isinstance(location, TheaterGroundObject): if not isinstance(location, TheaterGroundObject):
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
builder = WaypointBuilder(self.doctrine) builder = WaypointBuilder(flight, self.doctrine)
builder.ascent(flight.from_cp) builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight)) builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join) builder.join(self.package.waypoints.join)
@ -222,7 +222,7 @@ class FlightPlanBuilder:
) )
start = end.point_from_heading(heading - 180, diameter) start = end.point_from_heading(heading - 180, diameter)
builder = WaypointBuilder(self.doctrine) builder = WaypointBuilder(flight, self.doctrine)
builder.ascent(flight.from_cp) builder.ascent(flight.from_cp)
builder.race_track(start, end, patrol_alt) builder.race_track(start, end, patrol_alt)
builder.rtb(flight.from_cp) builder.rtb(flight.from_cp)
@ -264,7 +264,7 @@ class FlightPlanBuilder:
orbit1p = orbit_center.point_from_heading(heading + 180, radius) orbit1p = orbit_center.point_from_heading(heading + 180, radius)
# Create points # Create points
builder = WaypointBuilder(self.doctrine) builder = WaypointBuilder(flight, self.doctrine)
builder.ascent(flight.from_cp) builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight)) builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join) builder.join(self.package.waypoints.join)
@ -290,7 +290,7 @@ class FlightPlanBuilder:
if custom_targets is None: if custom_targets is None:
custom_targets = [] custom_targets = []
builder = WaypointBuilder(self.doctrine) builder = WaypointBuilder(flight, self.doctrine)
builder.ascent(flight.from_cp) builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight)) builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join) builder.join(self.package.waypoints.join)
@ -328,17 +328,12 @@ class FlightPlanBuilder:
def generate_escort(self, flight: Flight) -> None: def generate_escort(self, flight: Flight) -> None:
assert self.package.waypoints is not None assert self.package.waypoints is not None
patrol_alt = random.randint( builder = WaypointBuilder(flight, self.doctrine)
self.doctrine.min_patrol_altitude,
self.doctrine.max_patrol_altitude
)
builder = WaypointBuilder(self.doctrine)
builder.ascent(flight.from_cp) builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight)) builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join) builder.join(self.package.waypoints.join)
builder.race_track(self.package.waypoints.ingress, builder.escort(self.package.waypoints.ingress,
self.package.waypoints.egress, patrol_alt) self.package.target, self.package.waypoints.egress)
builder.split(self.package.waypoints.split) builder.split(self.package.waypoints.split)
builder.rtb(flight.from_cp) builder.rtb(flight.from_cp)
@ -366,7 +361,7 @@ class FlightPlanBuilder:
center = ingress.point_from_heading(heading, distance / 2) center = ingress.point_from_heading(heading, distance / 2)
egress = ingress.point_from_heading(heading, distance) egress = ingress.point_from_heading(heading, distance)
builder = WaypointBuilder(self.doctrine) builder = WaypointBuilder(flight, self.doctrine)
builder.ascent(flight.from_cp, is_helo) builder.ascent(flight.from_cp, is_helo)
builder.hold(self._hold_point(flight)) builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join) builder.join(self.package.waypoints.join)
@ -379,33 +374,39 @@ class FlightPlanBuilder:
flight.points = builder.build() flight.points = builder.build()
# TODO: Make a model for the waypoint builder and use that in the UI. # TODO: Make a model for the waypoint builder and use that in the UI.
def generate_ascend_point(self, departure: ControlPoint) -> FlightWaypoint: def generate_ascend_point(self, flight: Flight,
departure: ControlPoint) -> FlightWaypoint:
"""Generate ascend point. """Generate ascend point.
Args: Args:
flight: The flight to generate the descend point for.
departure: Departure airfield or carrier. departure: Departure airfield or carrier.
""" """
builder = WaypointBuilder(self.doctrine) builder = WaypointBuilder(flight, self.doctrine)
builder.ascent(departure) builder.ascent(departure)
return builder.build()[0] return builder.build()[0]
def generate_descend_point(self, arrival: ControlPoint) -> FlightWaypoint: def generate_descend_point(self, flight: Flight,
arrival: ControlPoint) -> FlightWaypoint:
"""Generate approach/descend point. """Generate approach/descend point.
Args: Args:
flight: The flight to generate the descend point for.
arrival: Arrival airfield or carrier. arrival: Arrival airfield or carrier.
""" """
builder = WaypointBuilder(self.doctrine) builder = WaypointBuilder(flight, self.doctrine)
builder.descent(arrival) builder.descent(arrival)
return builder.build()[0] return builder.build()[0]
def generate_rtb_waypoint(self, arrival: ControlPoint) -> FlightWaypoint: def generate_rtb_waypoint(self, flight: Flight,
arrival: ControlPoint) -> FlightWaypoint:
"""Generate RTB landing point. """Generate RTB landing point.
Args: Args:
flight: The flight to generate the landing waypoint for.
arrival: Arrival airfield or carrier. arrival: Arrival airfield or carrier.
""" """
builder = WaypointBuilder(self.doctrine) builder = WaypointBuilder(flight, self.doctrine)
builder.land(arrival) builder.land(arrival)
return builder.build()[0] return builder.build()[0]

View File

@ -22,12 +22,14 @@ CAP_DURATION = 30 # Minutes
INGRESS_TYPES = { INGRESS_TYPES = {
FlightWaypointType.INGRESS_CAS, FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_ESCORT,
FlightWaypointType.INGRESS_SEAD, FlightWaypointType.INGRESS_SEAD,
FlightWaypointType.INGRESS_STRIKE, FlightWaypointType.INGRESS_STRIKE,
} }
IP_TYPES = { IP_TYPES = {
FlightWaypointType.INGRESS_CAS, FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_ESCORT,
FlightWaypointType.INGRESS_SEAD, FlightWaypointType.INGRESS_SEAD,
FlightWaypointType.INGRESS_STRIKE, FlightWaypointType.INGRESS_STRIKE,
FlightWaypointType.PATROL_TRACK, FlightWaypointType.PATROL_TRACK,
@ -202,7 +204,11 @@ class TotEstimator:
@staticmethod @staticmethod
def estimate_startup(flight: Flight) -> int: def estimate_startup(flight: Flight) -> int:
if flight.start_type == "Cold": if flight.start_type == "Cold":
if flight.client_count:
return 10 * 60 return 10 * 60
else:
# The AI doesn't seem to have a real startup procedure.
return 2 * 60
return 0 return 0
@staticmethod @staticmethod

View File

@ -8,11 +8,12 @@ from dcs.unit import Unit
from game.data.doctrine import Doctrine from game.data.doctrine import Doctrine
from game.utils import nm_to_meter from game.utils import nm_to_meter
from theater import ControlPoint, MissionTarget, TheaterGroundObject from theater import ControlPoint, MissionTarget, TheaterGroundObject
from .flight import FlightWaypoint, FlightWaypointType from .flight import Flight, FlightWaypoint, FlightWaypointType
class WaypointBuilder: class WaypointBuilder:
def __init__(self, doctrine: Doctrine) -> None: def __init__(self, flight: Flight, doctrine: Doctrine) -> None:
self.flight = flight
self.doctrine = doctrine self.doctrine = doctrine
self.waypoints: List[FlightWaypoint] = [] self.waypoints: List[FlightWaypoint] = []
self.ingress_point: Optional[FlightWaypoint] = None self.ingress_point: Optional[FlightWaypoint] = None
@ -127,6 +128,9 @@ class WaypointBuilder:
def ingress_cas(self, position: Point, objective: MissionTarget) -> None: def ingress_cas(self, position: Point, objective: MissionTarget) -> None:
self._ingress(FlightWaypointType.INGRESS_CAS, position, objective) self._ingress(FlightWaypointType.INGRESS_CAS, position, objective)
def ingress_escort(self, position: Point, objective: MissionTarget) -> None:
self._ingress(FlightWaypointType.INGRESS_ESCORT, position, objective)
def ingress_sead(self, position: Point, objective: MissionTarget) -> None: def ingress_sead(self, position: Point, objective: MissionTarget) -> None:
self._ingress(FlightWaypointType.INGRESS_SEAD, position, objective) self._ingress(FlightWaypointType.INGRESS_SEAD, position, objective)
@ -199,6 +203,9 @@ class WaypointBuilder:
waypoint.description = description waypoint.description = description
waypoint.pretty_name = description waypoint.pretty_name = description
waypoint.name = name waypoint.name = name
# The target waypoints are only for the player's benefit. AI tasks for
# the target are set on the ingress point so they begin their attack
# *before* reaching the target.
waypoint.only_for_player = True waypoint.only_for_player = True
self.waypoints.append(waypoint) self.waypoints.append(waypoint)
# TODO: This seems wrong, but it's what was there before. # TODO: This seems wrong, but it's what was there before.
@ -231,6 +238,9 @@ class WaypointBuilder:
waypoint.description = name waypoint.description = name
waypoint.pretty_name = name waypoint.pretty_name = name
waypoint.name = name waypoint.name = name
# The target waypoints are only for the player's benefit. AI tasks for
# the target are set on the ingress point so they begin their attack
# *before* reaching the target.
waypoint.only_for_player = True waypoint.only_for_player = True
self.waypoints.append(waypoint) self.waypoints.append(waypoint)
# TODO: This seems wrong, but it's what was there before. # TODO: This seems wrong, but it's what was there before.
@ -305,3 +315,33 @@ class WaypointBuilder:
""" """
self.descent(arrival, is_helo) self.descent(arrival, is_helo)
self.land(arrival) self.land(arrival)
def escort(self, ingress: Point, target: MissionTarget,
egress: Point) -> None:
"""Creates the waypoints needed to escort the package.
Args:
ingress: The package ingress point.
target: The mission target.
egress: The package egress point.
"""
# This would preferably be no points at all, and instead the Escort task
# would begin on the join point and end on the split point, however the
# escort task does not appear to work properly (see the longer
# description in gen.aircraft.JoinPointBuilder), so instead we give
# the escort flights a flight plan including the ingress point, target
# area, and egress point.
self._ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
target.position.x,
target.position.y,
self.doctrine.ingress_altitude
)
waypoint.name = "TARGET"
waypoint.description = "Escort the package"
waypoint.pretty_name = "Target area"
self.waypoints.append(waypoint)
self.egress(egress, target)

View File

@ -252,6 +252,8 @@ class AtoModel(QAbstractListModel):
else: else:
self.ato = AirTaskingOrder() self.ato = AirTaskingOrder()
self.endResetModel() self.endResetModel()
# noinspection PyUnresolvedReferences
self.client_slots_changed.emit()
def get_package_model(self, index: QModelIndex) -> PackageModel: def get_package_model(self, index: QModelIndex) -> PackageModel:
"""Returns a model for the package at the given index.""" """Returns a model for the package at the given index."""

View File

@ -10,15 +10,26 @@ from PySide2.QtCore import (
QSize, QSize,
Qt, Qt,
) )
from PySide2.QtGui import QFont, QFontMetrics, QIcon, QPainter from PySide2.QtGui import (
QContextMenuEvent,
QFont,
QFontMetrics,
QIcon,
QPainter,
)
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QAbstractItemView, QAbstractItemView,
QAction,
QGroupBox, QGroupBox,
QHBoxLayout, QHBoxLayout,
QListView, QListView,
QMenu,
QPushButton, QPushButton,
QSplitter, QSplitter,
QStyle, QStyleOptionViewItem, QStyledItemDelegate, QVBoxLayout, QStyle,
QStyleOptionViewItem,
QStyledItemDelegate,
QVBoxLayout,
) )
from game import db from game import db
@ -130,14 +141,17 @@ class FlightDelegate(QStyledItemDelegate):
class QFlightList(QListView): class QFlightList(QListView):
"""List view for displaying the flights of a package.""" """List view for displaying the flights of a package."""
def __init__(self, model: Optional[PackageModel]) -> None: def __init__(self, game_model: GameModel,
package_model: Optional[PackageModel]) -> None:
super().__init__() super().__init__()
self.package_model = model self.game_model = game_model
self.set_package(model) self.package_model = package_model
if model is not None: self.set_package(package_model)
self.setItemDelegate(FlightDelegate(model.package)) if package_model is not None:
self.setItemDelegate(FlightDelegate(package_model.package))
self.setIconSize(QSize(91, 24)) self.setIconSize(QSize(91, 24))
self.setSelectionBehavior(QAbstractItemView.SelectItems) self.setSelectionBehavior(QAbstractItemView.SelectItems)
self.doubleClicked.connect(self.on_double_click)
def set_package(self, model: Optional[PackageModel]) -> None: def set_package(self, model: Optional[PackageModel]) -> None:
"""Sets the package model to display.""" """Sets the package model to display."""
@ -172,6 +186,38 @@ class QFlightList(QListView):
return None return None
return self.package_model.flight_at_index(index) return self.package_model.flight_at_index(index)
def on_double_click(self, index: QModelIndex) -> None:
if not index.isValid():
return
self.edit_flight(index)
def edit_flight(self, index: QModelIndex) -> None:
from qt_ui.dialogs import Dialog
Dialog.open_edit_flight_dialog(
self.package_model, self.package_model.flight_at_index(index)
)
def delete_flight(self, index: QModelIndex) -> None:
self.game_model.game.aircraft_inventory.return_from_flight(
self.selected_item)
self.package_model.delete_flight_at_index(index)
GameUpdateSignal.get_instance().redraw_flight_paths()
def contextMenuEvent(self, event: QContextMenuEvent) -> None:
index = self.indexAt(event.pos())
menu = QMenu("Menu")
edit_action = QAction("Edit")
edit_action.triggered.connect(lambda: self.edit_flight(index))
menu.addAction(edit_action)
delete_action = QAction(f"Delete")
delete_action.triggered.connect(lambda: self.delete_flight(index))
menu.addAction(delete_action)
menu.exec_(event.globalPos())
class QFlightPanel(QGroupBox): class QFlightPanel(QGroupBox):
"""The flight display portion of the ATO panel. """The flight display portion of the ATO panel.
@ -189,7 +235,7 @@ class QFlightPanel(QGroupBox):
self.vbox = QVBoxLayout() self.vbox = QVBoxLayout()
self.setLayout(self.vbox) self.setLayout(self.vbox)
self.flight_list = QFlightList(package_model) self.flight_list = QFlightList(game_model, package_model)
self.vbox.addWidget(self.flight_list) self.vbox.addWidget(self.flight_list)
self.button_row = QHBoxLayout() self.button_row = QHBoxLayout()
@ -242,10 +288,7 @@ class QFlightPanel(QGroupBox):
if not index.isValid(): if not index.isValid():
logging.error(f"Cannot edit flight when no flight is selected.") logging.error(f"Cannot edit flight when no flight is selected.")
return return
from qt_ui.dialogs import Dialog self.flight_list.edit_flight(index)
Dialog.open_edit_flight_dialog(
self.package_model, self.package_model.flight_at_index(index)
)
def on_delete(self) -> None: def on_delete(self) -> None:
"""Removes the selected flight from the package.""" """Removes the selected flight from the package."""
@ -253,10 +296,7 @@ class QFlightPanel(QGroupBox):
if not index.isValid(): if not index.isValid():
logging.error(f"Cannot delete flight when no flight is selected.") logging.error(f"Cannot delete flight when no flight is selected.")
return return
self.game_model.game.aircraft_inventory.return_from_flight( self.flight_list.delete_flight(index)
self.flight_list.selected_item)
self.package_model.delete_flight_at_index(index)
GameUpdateSignal.get_instance().redraw_flight_paths()
@contextmanager @contextmanager
@ -338,6 +378,7 @@ class QPackageList(QListView):
self.setIconSize(QSize(91, 24)) self.setIconSize(QSize(91, 24))
self.setSelectionBehavior(QAbstractItemView.SelectItems) self.setSelectionBehavior(QAbstractItemView.SelectItems)
self.model().rowsInserted.connect(self.on_new_packages) self.model().rowsInserted.connect(self.on_new_packages)
self.doubleClicked.connect(self.on_double_click)
@property @property
def selected_item(self) -> Optional[Package]: def selected_item(self) -> Optional[Package]:
@ -347,6 +388,14 @@ class QPackageList(QListView):
return None return None
return self.ato_model.package_at_index(index) return self.ato_model.package_at_index(index)
def edit_package(self, index: QModelIndex) -> None:
from qt_ui.dialogs import Dialog
Dialog.open_edit_package_dialog(self.ato_model.get_package_model(index))
def delete_package(self, index: QModelIndex) -> None:
self.ato_model.delete_package_at_index(index)
GameUpdateSignal.get_instance().redraw_flight_paths()
def on_new_packages(self, _parent: QModelIndex, first: int, def on_new_packages(self, _parent: QModelIndex, first: int,
_last: int) -> None: _last: int) -> None:
# Select the newly created pacakges. This should only ever happen due to # Select the newly created pacakges. This should only ever happen due to
@ -355,6 +404,26 @@ class QPackageList(QListView):
self.selectionModel().setCurrentIndex(self.model().index(first, 0), self.selectionModel().setCurrentIndex(self.model().index(first, 0),
QItemSelectionModel.Select) QItemSelectionModel.Select)
def on_double_click(self, index: QModelIndex) -> None:
if not index.isValid():
return
self.edit_package(index)
def contextMenuEvent(self, event: QContextMenuEvent) -> None:
index = self.indexAt(event.pos())
menu = QMenu("Menu")
edit_action = QAction("Edit")
edit_action.triggered.connect(lambda: self.edit_package(index))
menu.addAction(edit_action)
delete_action = QAction(f"Delete")
delete_action.triggered.connect(lambda: self.delete_package(index))
menu.addAction(delete_action)
menu.exec_(event.globalPos())
class QPackagePanel(QGroupBox): class QPackagePanel(QGroupBox):
"""The package display portion of the ATO panel. """The package display portion of the ATO panel.
@ -420,8 +489,7 @@ class QPackagePanel(QGroupBox):
if not index.isValid(): if not index.isValid():
logging.error(f"Cannot edit package when no package is selected.") logging.error(f"Cannot edit package when no package is selected.")
return return
from qt_ui.dialogs import Dialog self.package_list.edit_package(index)
Dialog.open_edit_package_dialog(self.ato_model.get_package_model(index))
def on_delete(self) -> None: def on_delete(self) -> None:
"""Removes the package from the ATO.""" """Removes the package from the ATO."""
@ -429,8 +497,7 @@ class QPackagePanel(QGroupBox):
if not index.isValid(): if not index.isValid():
logging.error(f"Cannot delete package when no package is selected.") logging.error(f"Cannot delete package when no package is selected.")
return return
self.ato_model.delete_package_at_index(index) self.package_list.delete_package(index)
GameUpdateSignal.get_instance().redraw_flight_paths()
class QAirTaskingOrderPanel(QSplitter): class QAirTaskingOrderPanel(QSplitter):
@ -440,6 +507,7 @@ class QAirTaskingOrderPanel(QSplitter):
packages of the player's ATO, and the bottom half displays the flights of packages of the player's ATO, and the bottom half displays the flights of
the selected package. the selected package.
""" """
def __init__(self, game_model: GameModel) -> None: def __init__(self, game_model: GameModel) -> None:
super().__init__(Qt.Vertical) super().__init__(Qt.Vertical)
self.ato_model = game_model.ato_model self.ato_model = game_model.ato_model

View File

@ -4,7 +4,7 @@ import webbrowser
from typing import Optional, Union from typing import Optional, Union
from PySide2.QtCore import Qt from PySide2.QtCore import Qt
from PySide2.QtGui import QIcon from PySide2.QtGui import QCloseEvent, QIcon
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QAction, QAction,
QActionGroup, QDesktopWidget, QActionGroup, QDesktopWidget,
@ -136,7 +136,7 @@ class QLiberationWindow(QMainWindow):
file_menu.addSeparator() file_menu.addSeparator()
file_menu.addAction(self.showLiberationPrefDialogAction) file_menu.addAction(self.showLiberationPrefDialogAction)
file_menu.addSeparator() file_menu.addSeparator()
file_menu.addAction("E&xit" , lambda: self.exit()) file_menu.addAction("E&xit", self.close)
displayMenu = self.menu.addMenu("&Display") displayMenu = self.menu.addMenu("&Display")
@ -214,13 +214,6 @@ class QLiberationWindow(QMainWindow):
self.game = game self.game = game
GameUpdateSignal.get_instance().updateGame(self.game) GameUpdateSignal.get_instance().updateGame(self.game)
def closeGame(self):
self.game = None
GameUpdateSignal.get_instance().updateGame(self.game)
def exit(self):
sys.exit(0)
def setGame(self, game: Optional[Game]): def setGame(self, game: Optional[Game]):
if game is not None: if game is not None:
game.on_load() game.on_load()
@ -257,3 +250,14 @@ class QLiberationWindow(QMainWindow):
logging.info("On Debriefing") logging.info("On Debriefing")
self.debriefing = QDebriefingWindow(debrief.debriefing, debrief.gameEvent, debrief.game) self.debriefing = QDebriefingWindow(debrief.debriefing, debrief.gameEvent, debrief.game)
self.debriefing.show() self.debriefing.show()
def closeEvent(self, event: QCloseEvent) -> None:
result = QMessageBox.question(
self, "Quit Liberation?",
"Are you sure you want to quit? All unsaved progress will be lost.",
QMessageBox.Yes | QMessageBox.No
)
if result == QMessageBox.Yes:
super().closeEvent(event)
else:
event.ignore()

View File

@ -17,7 +17,7 @@ from gen.ato import Package
from gen.flights.flight import Flight from gen.flights.flight import Flight
from gen.flights.flightplan import FlightPlanBuilder from gen.flights.flightplan import FlightPlanBuilder
from gen.flights.traveltime import TotEstimator from gen.flights.traveltime import TotEstimator
from qt_ui.models import AtoModel, PackageModel from qt_ui.models import AtoModel, GameModel, PackageModel
from qt_ui.uiconstants import EVENT_ICONS from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.ato import QFlightList from qt_ui.widgets.ato import QFlightList
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
@ -35,9 +35,9 @@ class QPackageDialog(QDialog):
#: Emitted when a change is made to the package. #: Emitted when a change is made to the package.
package_changed = Signal() package_changed = Signal()
def __init__(self, game: Game, model: PackageModel) -> None: def __init__(self, game_model: GameModel, model: PackageModel) -> None:
super().__init__() super().__init__()
self.game = game self.game_model = game_model
self.package_model = model self.package_model = model
self.add_flight_dialog: Optional[QFlightCreator] = None self.add_flight_dialog: Optional[QFlightCreator] = None
@ -86,7 +86,7 @@ class QPackageDialog(QDialog):
self.reset_tot_button.clicked.connect(self.reset_tot) self.reset_tot_button.clicked.connect(self.reset_tot)
self.tot_column.addWidget(self.reset_tot_button) self.tot_column.addWidget(self.reset_tot_button)
self.package_view = QFlightList(self.package_model) self.package_view = QFlightList(self.game_model, self.package_model)
self.package_view.selectionModel().selectionChanged.connect( self.package_view.selectionModel().selectionChanged.connect(
self.on_selection_changed self.on_selection_changed
) )
@ -113,6 +113,10 @@ class QPackageDialog(QDialog):
self.finished.connect(self.on_close) self.finished.connect(self.on_close)
self.rejected.connect(self.on_cancel) self.rejected.connect(self.on_cancel)
@property
def game(self) -> Game:
return self.game_model.game
def tot_qtime(self) -> QTime: def tot_qtime(self) -> QTime:
delay = self.package_model.package.time_over_target delay = self.package_model.package.time_over_target
hours = delay // 3600 hours = delay // 3600

View File

@ -111,19 +111,22 @@ class QFlightWaypointTab(QFrame):
self.subwindow.show() self.subwindow.show()
def on_ascend_waypoint(self): def on_ascend_waypoint(self):
ascend = self.planner.generate_ascend_point(self.flight.from_cp) ascend = self.planner.generate_ascend_point(self.flight,
self.flight.from_cp)
self.flight.points.append(ascend) self.flight.points.append(ascend)
self.flight_waypoint_list.update_list() self.flight_waypoint_list.update_list()
self.on_change() self.on_change()
def on_rtb_waypoint(self): def on_rtb_waypoint(self):
rtb = self.planner.generate_rtb_waypoint(self.flight.from_cp) rtb = self.planner.generate_rtb_waypoint(self.flight,
self.flight.from_cp)
self.flight.points.append(rtb) self.flight.points.append(rtb)
self.flight_waypoint_list.update_list() self.flight_waypoint_list.update_list()
self.on_change() self.on_change()
def on_descend_waypoint(self): def on_descend_waypoint(self):
descend = self.planner.generate_descend_point(self.flight.from_cp) descend = self.planner.generate_descend_point(self.flight,
self.flight.from_cp)
self.flight.points.append(descend) self.flight.points.append(descend)
self.flight_waypoint_list.update_list() self.flight_waypoint_list.update_list()
self.on_change() self.on_change()

View File

@ -1,3 +1,6 @@
-- the state.json file will be updated according to this schedule, and also on each destruction or capture event
local WRITESTATE_SCHEDULE_IN_SECONDS = 60
logger = mist.Logger:new("DCSLiberation", "info") logger = mist.Logger:new("DCSLiberation", "info")
logger:info("Check that json.lua is loaded : json = "..tostring(json)) logger:info("Check that json.lua is loaded : json = "..tostring(json))
@ -8,6 +11,10 @@ base_capture_events = {}
destroyed_objects_positions = {} destroyed_objects_positions = {}
mission_ended = false mission_ended = false
local function ends_with(str, ending)
return ending == "" or str:sub(-#ending) == ending
end
local function messageAll(message) local function messageAll(message)
local msg = {} local msg = {}
msg.text = message msg.text = message
@ -16,10 +23,13 @@ local function messageAll(message)
mist.message.add(msg) mist.message.add(msg)
end end
write_state = function() function write_state()
--messageAll("Writing DCS Liberation State...") local _debriefing_file_location = debriefing_file_location
--logger.info("Writing DCS LIBERATION state") if not debriefing_file_location then
local fp = io.open(debriefing_file_location, 'w') _debriefing_file_location = "[nil]"
end
local fp = io.open(_debriefing_file_location, 'w')
local game_state = { local game_state = {
["killed_aircrafts"] = killed_aircrafts, ["killed_aircrafts"] = killed_aircrafts,
["killed_ground_units"] = killed_ground_units, ["killed_ground_units"] = killed_ground_units,
@ -29,83 +39,114 @@ write_state = function()
["destroyed_objects_positions"] = destroyed_objects_positions, ["destroyed_objects_positions"] = destroyed_objects_positions,
} }
if not json then if not json then
local message = string.format("Unable to save DCS Liberation state to %s, JSON library is not loaded !",debriefing_file_location) local message = string.format("Unable to save DCS Liberation state to %s, JSON library is not loaded !", _debriefing_file_location)
logger:error(message) logger:error(message)
messageAll(message) messageAll(message)
end end
fp:write(json:encode(game_state)) fp:write(json:encode(game_state))
fp:close() fp:close()
-- logger.info("Done writing DCS Liberation state")
-- messageAll("Done writing DCS Liberation state.")
end end
local function canWrite(name)
local f = io.open(name, "w")
if f then
f:close()
return true
end
return false
end
local function testDebriefingFilePath(folderPath, folderName, useCurrentStamping)
if folderPath then
local filePath = nil
if not ends_with(folderPath, "\\") then
folderPath = folderPath .. "\\"
end
if useCurrentStamping then
filePath = string.format("%sstate-%s.json",folderPath, tostring(os.time()))
else
filePath = string.format("%sstate.json",folderPath)
end
local isOk = canWrite(filePath)
if isOk then
logger:info(string.format("The state.json file will be created in %s : (%s)",folderName, filePath))
return filePath
end
end
return nil
end
local function discoverDebriefingFilePath() local function discoverDebriefingFilePath()
local function insertFileName(directoryOrFilePath, overrideFileName)
if overrideFileName then
logger:info("Using LIBERATION_EXPORT_STAMPED_STATE to locate the state.json")
return directoryOrFilePath .. os.time() .. "-state.json"
end
local filename = "state.json"
if not (directoryOrFilePath:sub(-#filename) == filename) then
return directoryOrFilePath .. filename
end
return directoryOrFilePath
end
-- establish a search pattern into the following modes -- establish a search pattern into the following modes
-- 1. Environment variable mode, to support dedicated server hosting -- 1. Environment variable LIBERATION_EXPORT_DIR, to support dedicated server hosting
-- 2. Embedded DCS Liberation Generation, to support locally hosted single player -- 2. Embedded DCS Liberation dcsLiberation.installPath (set by the app to its install path), to support locally hosted single player
-- 3. Retain the classic TEMP directory logic -- 3. System temporary folder, as set in the TEMP environment variable
-- 4. Working directory.
local useCurrentStamping = nil
if os then
useCurrentStamping = os.getenv("LIBERATION_EXPORT_STAMPED_STATE")
end
local installPath = nil
if dcsLiberation then
installPath = dcsLiberation.installPath
end
if os then if os then
local exportDirectory = os.getenv("LIBERATION_EXPORT_DIR") local result = nil
-- try using the LIBERATION_EXPORT_DIR environment variable
if exportDirectory then result = testDebriefingFilePath(os.getenv("LIBERATION_EXPORT_DIR"), "LIBERATION_EXPORT_DIR", useCurrentStamping)
logger:info("Using LIBERATION_EXPORT_DIR to locate the state.json") if result then
local useCurrentStamping = os.getenv("LIBERATION_EXPORT_STAMPED_STATE") return result
exportDirectory = exportDirectory .. "\\" end
return insertFileName(exportDirectory, useCurrentStamping) -- no joy ? maybe there is a valid path in the mission ?
result = testDebriefingFilePath(installPath, "the DCS Liberation install folder", useCurrentStamping)
if result then
return result
end
-- there's always the possibility of using the system temporary folder
result = testDebriefingFilePath(os.getenv("TEMP"), "TEMP", useCurrentStamping)
if result then
return result
end end
end end
if dcsLiberation then -- nothing worked, let's try the last resort folder : current directory.
logger:info("Using DCS Liberation install folder for state.json")
return insertFileName(dcsLiberation.installPath)
end
if lfs then if lfs then
logger:info("Using DCS working directory for state.json") return testDebriefingFilePath(lfs.writedir(), "the working directory", useCurrentStamping)
return insertFileName(lfs.writedir())
end
end end
return nil
end
debriefing_file_location = discoverDebriefingFilePath() debriefing_file_location = discoverDebriefingFilePath()
logger:info(string.format("DCS Liberation state will be written as json to [[%s]]",debriefing_file_location))
write_state_error_handling = function() write_state_error_handling = function()
local _debriefing_file_location = debriefing_file_location
if not debriefing_file_location then
_debriefing_file_location = "[nil]"
logger:error("Unable to find where to write DCS Liberation state")
end
if pcall(write_state) then if pcall(write_state) then
-- messageAll("Written DCS Liberation state to "..debriefing_file_location)
else else
messageAll("Unable to write DCS Liberation state to "..debriefing_file_location.. messageAll("Unable to write DCS Liberation state to ".._debriefing_file_location..
"\nYou can abort the mission in DCS Liberation.\n".. "\nYou can abort the mission in DCS Liberation.\n"..
"\n\nPlease fix your setup in DCS Liberation, make sure you are pointing to the right installation directory from the File/Preferences menu. Then after fixing the path restart DCS Liberation, and then restart DCS.".. "\n\nPlease fix your setup in DCS Liberation, make sure you are pointing to the right installation directory from the File/Preferences menu. Then after fixing the path restart DCS Liberation, and then restart DCS."..
"\n\nYou can also try to fix the issue manually by replacing the file <dcs_installation_directory>/Scripts/MissionScripting.lua by the one provided there : <dcs_liberation_folder>/resources/scripts/MissionScripting.lua. And then restart DCS. (This will also have to be done again after each DCS update)".. "\n\nYou can also try to fix the issue manually by replacing the file <dcs_installation_directory>/Scripts/MissionScripting.lua by the one provided there : <dcs_liberation_folder>/resources/scripts/MissionScripting.lua. And then restart DCS. (This will also have to be done again after each DCS update)"..
"\n\nIt's not worth playing, the state of the mission will not be recorded.") "\n\nIt's not worth playing, the state of the mission will not be recorded.")
end end
end
mist.scheduleFunction(write_state_error_handling, {}, timer.getTime() + 10, 60, timer.getTime() + 3600) -- reschedule
mist.scheduleFunction(write_state_error_handling, {}, timer.getTime() + WRITESTATE_SCHEDULE_IN_SECONDS)
end
activeWeapons = {} activeWeapons = {}
local function onEvent(event) local function onEvent(event)
if event.id == world.event.S_EVENT_CRASH and event.initiator then if event.id == world.event.S_EVENT_CRASH and event.initiator then
--messageAll("Destroyed :" .. event.initiator.getName(event.initiator))
killed_aircrafts[#killed_aircrafts + 1] = event.initiator.getName(event.initiator) killed_aircrafts[#killed_aircrafts + 1] = event.initiator.getName(event.initiator)
write_state()
end end
if event.id == world.event.S_EVENT_DEAD and event.initiator then if event.id == world.event.S_EVENT_DEAD and event.initiator then
@ -118,15 +159,12 @@ local function onEvent(event)
destruction.type = event.initiator:getTypeName() destruction.type = event.initiator:getTypeName()
destruction.orientation = mist.getHeading(event.initiator) * 57.3 destruction.orientation = mist.getHeading(event.initiator) * 57.3
destroyed_objects_positions[#destroyed_objects_positions + 1] = destruction destroyed_objects_positions[#destroyed_objects_positions + 1] = destruction
write_state()
end end
--if event.id == world.event.S_EVENT_SHOT and event.weapon then
-- weapons_fired[#weapons_fired + 1] = event.weapon.getTypeName(event.weapon)
--end
if event.id == world.event.S_EVENT_BASE_CAPTURED and event.place then if event.id == world.event.S_EVENT_BASE_CAPTURED and event.place then
--messageAll("Base captured :" .. event.place.getName(event.place))
base_capture_events[#base_capture_events + 1] = event.place.getID(event.place) .. "||" .. event.place.getCoalition(event.place) .. "||" .. event.place.getName(event.place) base_capture_events[#base_capture_events + 1] = event.place.getID(event.place) .. "||" .. event.place.getCoalition(event.place) .. "||" .. event.place.getName(event.place)
write_state()
end end
if event.id == world.event.S_EVENT_MISSION_END then if event.id == world.event.S_EVENT_MISSION_END then
@ -137,3 +175,6 @@ local function onEvent(event)
end end
mist.addEventHandler(onEvent) mist.addEventHandler(onEvent)
-- create the state.json file and start the scheduling
write_state_error_handling()

View File

@ -9,17 +9,17 @@
env.info("DCSLiberation|JTACAutolase plugin - configuration") env.info("DCSLiberation|JTACAutolase plugin - configuration")
if dcsLiberation then if dcsLiberation then
env.info(string.format("DCSLiberation|JTACAutolase plugin - dcsLiberation")) env.info("DCSLiberation|JTACAutolase plugin - dcsLiberation")
-- specific options -- specific options
local smoke = false local smoke = false
-- retrieve specific options values -- retrieve specific options values
if dcsLiberation.plugins then if dcsLiberation.plugins then
env.info(string.format("DCSLiberation|JTACAutolase plugin - dcsLiberation.plugins")) env.info("DCSLiberation|JTACAutolase plugin - dcsLiberation.plugins")
if dcsLiberation.plugins.jtacautolase then if dcsLiberation.plugins.jtacautolase then
env.info(string.format("DCSLiberation|JTACAutolase plugin - dcsLiberation.plugins.jtacautolase")) env.info("DCSLiberation|JTACAutolase plugin - dcsLiberation.plugins.jtacautolase")
smoke = dcsLiberation.plugins.jtacautolase.smoke smoke = dcsLiberation.plugins.jtacautolase.smoke
env.info(string.format("DCSLiberation|JTACAutolase plugin - smoke = %s",tostring(smoke))) env.info(string.format("DCSLiberation|JTACAutolase plugin - smoke = %s",tostring(smoke)))
end end
@ -29,7 +29,7 @@ if dcsLiberation then
for _, jtac in pairs(dcsLiberation.JTACs) do for _, jtac in pairs(dcsLiberation.JTACs) do
env.info(string.format("DCSLiberation|JTACAutolase plugin - setting up %s",jtac.dcsUnit)) env.info(string.format("DCSLiberation|JTACAutolase plugin - setting up %s",jtac.dcsUnit))
if JTACAutoLase then if JTACAutoLase then
env.info(string.format("DCSLiberation|JTACAutolase plugin - calling dcsLiberation.JTACAutoLase")) env.info("DCSLiberation|JTACAutolase plugin - calling JTACAutoLase")
JTACAutoLase(jtac.dcsUnit, jtac.laserCode, smoke, 'vehicle') JTACAutoLase(jtac.dcsUnit, jtac.laserCode, smoke, 'vehicle')
end end
end end