mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Merge branch 'develop' into faction_refactor
This commit is contained in:
commit
d6b94345d9
@ -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")
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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]
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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."""
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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()
|
||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user