mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge branch 'develop' into faction_refactor
This commit is contained in:
commit
6a91fad10a
1
.gitignore
vendored
1
.gitignore
vendored
@ -21,4 +21,3 @@ logs/
|
||||
qt_ui/logs/liberation.log
|
||||
|
||||
*.psd
|
||||
resources/scripts/plugins/*
|
||||
|
||||
@ -12,7 +12,6 @@ from game import db, persistency
|
||||
from game.debriefing import Debriefing
|
||||
from game.infos.information import Information
|
||||
from game.operation.operation import Operation
|
||||
from gen.environmentgen import EnvironmentSettings
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from theater import ControlPoint
|
||||
from theater.start_generator import generate_airbase_defense_group
|
||||
@ -42,7 +41,6 @@ class Event:
|
||||
|
||||
operation = None # type: Operation
|
||||
difficulty = 1 # type: int
|
||||
environment_settings = None # type: EnvironmentSettings
|
||||
BONUS_BASE = 5
|
||||
|
||||
def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str):
|
||||
|
||||
29
game/game.py
29
game/game.py
@ -2,7 +2,7 @@ import logging
|
||||
import math
|
||||
import random
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from dcs.action import Coalition
|
||||
@ -28,6 +28,8 @@ from .event.event import Event, UnitsDeliveryEvent
|
||||
from .event.frontlineattack import FrontlineAttackEvent
|
||||
from .infos.information import Information
|
||||
from .settings import Settings
|
||||
from plugin import LuaPluginManager
|
||||
from .weather import Conditions, TimeOfDay
|
||||
|
||||
COMMISION_UNIT_VARIETY = 4
|
||||
COMMISION_LIMITS_SCALE = 1.5
|
||||
@ -78,7 +80,7 @@ class Game:
|
||||
self.enemy_name = enemy_name
|
||||
self.enemy_country = db.FACTIONS[enemy_name]["country"]
|
||||
self.turn = 0
|
||||
self.date = datetime(start_date.year, start_date.month, start_date.day)
|
||||
self.date = date(start_date.year, start_date.month, start_date.day)
|
||||
self.game_stats = GameStats()
|
||||
self.game_stats.update(self)
|
||||
self.ground_planners: Dict[int, GroundPlanner] = {}
|
||||
@ -91,6 +93,8 @@ class Game:
|
||||
self.current_unit_id = 0
|
||||
self.current_group_id = 0
|
||||
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
self.blue_ato = AirTaskingOrder()
|
||||
self.red_ato = AirTaskingOrder()
|
||||
|
||||
@ -101,6 +105,9 @@ class Game:
|
||||
self.sanitize_sides()
|
||||
self.on_load()
|
||||
|
||||
def generate_conditions(self) -> Conditions:
|
||||
return Conditions.generate(self.theater, self.date,
|
||||
self.current_turn_time_of_day, self.settings)
|
||||
|
||||
def sanitize_sides(self):
|
||||
"""
|
||||
@ -217,6 +224,16 @@ class Game:
|
||||
|
||||
def on_load(self) -> None:
|
||||
ObjectiveDistanceCache.set_theater(self.theater)
|
||||
|
||||
# set the settings in all plugins
|
||||
for plugin in LuaPluginManager().getPlugins():
|
||||
plugin.setSettings(self.settings)
|
||||
|
||||
# Save game compatibility.
|
||||
|
||||
# TODO: Remove in 2.3.
|
||||
if not hasattr(self, "conditions"):
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
def pass_turn(self, no_action=False):
|
||||
logging.info("Pass turn")
|
||||
@ -252,6 +269,8 @@ class Game:
|
||||
for cp in self.theater.controlpoints:
|
||||
self.aircraft_inventory.set_from_control_point(cp)
|
||||
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
# Plan flights & combat for next turn
|
||||
self.__culling_points = self.compute_conflicts_position()
|
||||
self.ground_planners = {}
|
||||
@ -340,11 +359,11 @@ class Game:
|
||||
self.informations.append(info)
|
||||
|
||||
@property
|
||||
def current_turn_daytime(self):
|
||||
return ["dawn", "day", "dusk", "night"][self.turn % 4]
|
||||
def current_turn_time_of_day(self) -> TimeOfDay:
|
||||
return list(TimeOfDay)[self.turn % 4]
|
||||
|
||||
@property
|
||||
def current_day(self):
|
||||
def current_day(self) -> date:
|
||||
return self.date + timedelta(days=self.turn // 4)
|
||||
|
||||
def next_unit_id(self):
|
||||
|
||||
@ -65,16 +65,6 @@ class ControlPointAircraftInventory:
|
||||
if count > 0:
|
||||
yield aircraft, count
|
||||
|
||||
@property
|
||||
def total_available(self) -> int:
|
||||
"""Returns the total number of aircraft available."""
|
||||
# TODO: Remove?
|
||||
# This probably isn't actually useful. It's used by the AI flight
|
||||
# planner to determine how many flights of a given type it should
|
||||
# allocate, but it should probably be making that decision based on the
|
||||
# number of aircraft available to perform a particular role.
|
||||
return sum(self.inventory.values())
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clears all aircraft from the inventory."""
|
||||
self.inventory.clear()
|
||||
|
||||
@ -14,14 +14,14 @@ from dcs.translation import String
|
||||
from dcs.triggers import TriggerStart
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
from gen import Conflict, VisualGenerator
|
||||
from gen import Conflict, VisualGenerator, FlightType
|
||||
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
|
||||
from gen.airfields import AIRFIELD_DATA
|
||||
from gen.airsupportgen import AirSupport, AirSupportConflictGenerator
|
||||
from gen.armor import GroundConflictGenerator, JtacInfo
|
||||
from gen.beacons import load_beacons_for_terrain
|
||||
from gen.briefinggen import BriefingGenerator
|
||||
from gen.environmentgen import EnviromentGenerator
|
||||
from gen.environmentgen import EnvironmentGenerator
|
||||
from gen.forcedoptionsgen import ForcedOptionsGenerator
|
||||
from gen.groundobjectsgen import GroundObjectsGenerator
|
||||
from gen.kneeboard import KneeboardGenerator
|
||||
@ -31,7 +31,7 @@ from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
|
||||
from theater import ControlPoint
|
||||
from .. import db
|
||||
from ..debriefing import Debriefing
|
||||
|
||||
from plugin import LuaPluginManager
|
||||
|
||||
class Operation:
|
||||
attackers_starting_position = None # type: db.StartingPosition
|
||||
@ -45,7 +45,6 @@ class Operation:
|
||||
triggersgen = None # type: TriggersGenerator
|
||||
airsupportgen = None # type: AirSupportConflictGenerator
|
||||
visualgen = None # type: VisualGenerator
|
||||
envgen = None # type: EnviromentGenerator
|
||||
groundobjectgen = None # type: GroundObjectsGenerator
|
||||
briefinggen = None # type: BriefingGenerator
|
||||
forcedoptionsgen = None # type: ForcedOptionsGenerator
|
||||
@ -75,6 +74,7 @@ class Operation:
|
||||
self.departure_cp = departure_cp
|
||||
self.to_cp = to_cp
|
||||
self.is_quick = False
|
||||
self.listOfPluginsScripts = []
|
||||
|
||||
def units_of(self, country_name: str) -> List[UnitType]:
|
||||
return []
|
||||
@ -133,6 +133,34 @@ class Operation:
|
||||
else:
|
||||
self.defenders_starting_position = None
|
||||
|
||||
def injectLuaTrigger(self, luascript, comment = "LUA script"):
|
||||
trigger = TriggerStart(comment=comment)
|
||||
trigger.add_action(DoScript(String(luascript)))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
def bypassPluginScript(self, pluginName, scriptFileMnemonic):
|
||||
self.listOfPluginsScripts.append(scriptFileMnemonic)
|
||||
|
||||
def injectPluginScript(self, pluginName, scriptFile, scriptFileMnemonic):
|
||||
if not scriptFileMnemonic in self.listOfPluginsScripts:
|
||||
self.listOfPluginsScripts.append(scriptFileMnemonic)
|
||||
|
||||
plugin_path = Path("./resources/plugins",pluginName)
|
||||
|
||||
if scriptFile != None:
|
||||
scriptFile_path = Path(plugin_path, scriptFile)
|
||||
if scriptFile_path.exists():
|
||||
trigger = TriggerStart(comment="Load " + scriptFileMnemonic)
|
||||
filename = scriptFile_path.resolve()
|
||||
fileref = self.current_mission.map_resource.add_resource_file(filename)
|
||||
trigger.add_action(DoScriptFile(fileref))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
else:
|
||||
logging.error(f"Cannot find script file {scriptFile} for plugin {pluginName}")
|
||||
|
||||
else:
|
||||
logging.debug(f"Skipping script file {scriptFile} for plugin {pluginName}")
|
||||
|
||||
def generate(self):
|
||||
radio_registry = RadioRegistry()
|
||||
tacan_registry = TacanRegistry()
|
||||
@ -162,13 +190,9 @@ class Operation:
|
||||
for frequency in unique_map_frequencies:
|
||||
radio_registry.reserve(frequency)
|
||||
|
||||
# Generate meteo
|
||||
envgen = EnviromentGenerator(self.current_mission, self.conflict,
|
||||
self.game)
|
||||
if self.environment_settings is None:
|
||||
self.environment_settings = envgen.generate()
|
||||
else:
|
||||
envgen.load(self.environment_settings)
|
||||
# Set mission time and weather conditions.
|
||||
EnvironmentGenerator(self.current_mission,
|
||||
self.game.conditions).generate()
|
||||
|
||||
# Generate ground object first
|
||||
|
||||
@ -259,98 +283,181 @@ class Operation:
|
||||
if self.game.settings.perf_smoke_gen:
|
||||
visualgen.generate()
|
||||
|
||||
# Inject Plugins Lua Scripts
|
||||
listOfPluginsScripts = []
|
||||
plugin_file_path = Path("./resources/scripts/plugins/__plugins.lst")
|
||||
if plugin_file_path.exists():
|
||||
for line in plugin_file_path.read_text().splitlines():
|
||||
name = line.strip()
|
||||
if not name.startswith( '#' ):
|
||||
trigger = TriggerStart(comment="Load " + name)
|
||||
listOfPluginsScripts.append(name)
|
||||
fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/plugins/" + name)
|
||||
trigger.add_action(DoScriptFile(fileref))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
else:
|
||||
logging.info(
|
||||
f"Not loading plugins, {plugin_file_path} does not exist")
|
||||
luaData = {}
|
||||
luaData["AircraftCarriers"] = {}
|
||||
luaData["Tankers"] = {}
|
||||
luaData["AWACs"] = {}
|
||||
luaData["JTACs"] = {}
|
||||
luaData["TargetPoints"] = {}
|
||||
|
||||
# Inject Mist Script if not done already in the plugins
|
||||
if not "mist.lua" in listOfPluginsScripts and not "mist_4_3_74.lua" in listOfPluginsScripts: # don't load the script twice
|
||||
trigger = TriggerStart(comment="Load Mist Lua framework")
|
||||
fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/mist_4_3_74.lua")
|
||||
trigger.add_action(DoScriptFile(fileref))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
self.assign_channels_to_flights(airgen.flights,
|
||||
airsupportgen.air_support)
|
||||
|
||||
# Inject JSON library if not done already in the plugins
|
||||
if not "json.lua" in listOfPluginsScripts : # don't load the script twice
|
||||
trigger = TriggerStart(comment="Load JSON Lua library")
|
||||
fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/json.lua")
|
||||
trigger.add_action(DoScriptFile(fileref))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
kneeboard_generator = KneeboardGenerator(self.current_mission)
|
||||
for dynamic_runway in groundobjectgen.runways.values():
|
||||
self.briefinggen.add_dynamic_runway(dynamic_runway)
|
||||
|
||||
for tanker in airsupportgen.air_support.tankers:
|
||||
self.briefinggen.add_tanker(tanker)
|
||||
kneeboard_generator.add_tanker(tanker)
|
||||
luaData["Tankers"][tanker.callsign] = {
|
||||
"dcsGroupName": tanker.dcsGroupName,
|
||||
"callsign": tanker.callsign,
|
||||
"variant": tanker.variant,
|
||||
"radio": tanker.freq.mhz,
|
||||
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name
|
||||
}
|
||||
|
||||
if self.is_awacs_enabled:
|
||||
for awacs in airsupportgen.air_support.awacs:
|
||||
self.briefinggen.add_awacs(awacs)
|
||||
kneeboard_generator.add_awacs(awacs)
|
||||
luaData["AWACs"][awacs.callsign] = {
|
||||
"dcsGroupName": awacs.dcsGroupName,
|
||||
"callsign": awacs.callsign,
|
||||
"radio": awacs.freq.mhz
|
||||
}
|
||||
|
||||
for jtac in jtacs:
|
||||
self.briefinggen.add_jtac(jtac)
|
||||
kneeboard_generator.add_jtac(jtac)
|
||||
luaData["JTACs"][jtac.callsign] = {
|
||||
"dcsGroupName": jtac.dcsGroupName,
|
||||
"callsign": jtac.callsign,
|
||||
"zone": jtac.region,
|
||||
"dcsUnit": jtac.unit_name,
|
||||
"laserCode": jtac.code
|
||||
}
|
||||
|
||||
for flight in airgen.flights:
|
||||
self.briefinggen.add_flight(flight)
|
||||
kneeboard_generator.add_flight(flight)
|
||||
if flight.friendly and flight.flight_type in [FlightType.ANTISHIP, FlightType.DEAD, FlightType.SEAD, FlightType.STRIKE]:
|
||||
flightType = flight.flight_type.name
|
||||
flightTarget = flight.targetPoint
|
||||
if flightTarget:
|
||||
flightTargetName = None
|
||||
flightTargetType = None
|
||||
if hasattr(flightTarget, 'obj_name'):
|
||||
flightTargetName = flightTarget.obj_name
|
||||
flightTargetType = flightType + f" TGT ({flightTarget.category})"
|
||||
elif hasattr(flightTarget, 'name'):
|
||||
flightTargetName = flightTarget.name
|
||||
flightTargetType = flightType + " TGT (Airbase)"
|
||||
luaData["TargetPoints"][flightTargetName] = {
|
||||
"name": flightTargetName,
|
||||
"type": flightTargetType,
|
||||
"position": { "x": flightTarget.position.x, "y": flightTarget.position.y}
|
||||
}
|
||||
|
||||
|
||||
self.briefinggen.generate()
|
||||
kneeboard_generator.generate()
|
||||
|
||||
# Inject Ciribob's JTACAutoLase if not done already in the plugins
|
||||
if not "JTACAutoLase.lua" in listOfPluginsScripts : # don't load the script twice
|
||||
trigger = TriggerStart(comment="Load JTACAutoLase.lua script")
|
||||
fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/JTACAutoLase.lua")
|
||||
trigger.add_action(DoScriptFile(fileref))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
# set a LUA table with data from Liberation that we want to set
|
||||
# at the moment it contains Liberation's install path, and an overridable definition for the JTACAutoLase function
|
||||
# later, we'll add data about the units and points having been generated, in order to facilitate the configuration of the plugin lua scripts
|
||||
state_location = "[[" + os.path.abspath("state.json") + "]]"
|
||||
lua = """
|
||||
-- setting configuration table
|
||||
env.info("DCSLiberation|: setting configuration table")
|
||||
|
||||
-- all data in this table is overridable.
|
||||
dcsLiberation = {}
|
||||
|
||||
-- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory
|
||||
dcsLiberation.installPath=""" + state_location + """
|
||||
|
||||
-- you can override dcsLiberation.JTACAutoLase to make it use your own function ; it will be called with these parameters : ({jtac.unit_name}, {jtac.code}, {smoke}, 'vehicle') for all JTACs
|
||||
if ctld then
|
||||
dcsLiberation.JTACAutoLase=ctld.JTACAutoLase
|
||||
elseif JTACAutoLase then
|
||||
dcsLiberation.JTACAutoLase=JTACAutoLase
|
||||
end
|
||||
|
||||
-- later, we'll add more data to the table
|
||||
--dcsLiberation.POIs = {}
|
||||
--dcsLiberation.BASEs = {}
|
||||
--dcsLiberation.JTACs = {}
|
||||
"""
|
||||
-- setting configuration table
|
||||
env.info("DCSLiberation|: setting configuration table")
|
||||
|
||||
-- all data in this table is overridable.
|
||||
dcsLiberation = {}
|
||||
|
||||
-- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory
|
||||
dcsLiberation.installPath=""" + state_location + """
|
||||
|
||||
"""
|
||||
# Process the tankers
|
||||
lua += """
|
||||
|
||||
-- list the tankers generated by Liberation
|
||||
dcsLiberation.Tankers = {
|
||||
"""
|
||||
for key in luaData["Tankers"]:
|
||||
data = luaData["Tankers"][key]
|
||||
dcsGroupName= data["dcsGroupName"]
|
||||
callsign = data["callsign"]
|
||||
variant = data["variant"]
|
||||
tacan = data["tacan"]
|
||||
radio = data["radio"]
|
||||
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', variant='{variant}', tacan='{tacan}', radio='{radio}' }}, \n"
|
||||
#lua += f" {{name='{dcsGroupName}', description='{callsign} ({variant})', information='Tacan:{tacan} Radio:{radio}' }}, \n"
|
||||
lua += "}"
|
||||
|
||||
# Process the AWACSes
|
||||
lua += """
|
||||
|
||||
-- list the AWACs generated by Liberation
|
||||
dcsLiberation.AWACs = {
|
||||
"""
|
||||
for key in luaData["AWACs"]:
|
||||
data = luaData["AWACs"][key]
|
||||
dcsGroupName= data["dcsGroupName"]
|
||||
callsign = data["callsign"]
|
||||
radio = data["radio"]
|
||||
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', radio='{radio}' }}, \n"
|
||||
#lua += f" {{name='{dcsGroupName}', description='{callsign} (AWACS)', information='Radio:{radio}' }}, \n"
|
||||
lua += "}"
|
||||
|
||||
# Process the JTACs
|
||||
lua += """
|
||||
|
||||
-- list the JTACs generated by Liberation
|
||||
dcsLiberation.JTACs = {
|
||||
"""
|
||||
for key in luaData["JTACs"]:
|
||||
data = luaData["JTACs"][key]
|
||||
dcsGroupName= data["dcsGroupName"]
|
||||
callsign = data["callsign"]
|
||||
zone = data["zone"]
|
||||
laserCode = data["laserCode"]
|
||||
dcsUnit = data["dcsUnit"]
|
||||
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', zone='{zone}', laserCode='{laserCode}', dcsUnit='{dcsUnit}' }}, \n"
|
||||
#lua += f" {{name='{dcsGroupName}', description='JTAC {callsign} ', information='Laser:{laserCode}', jtac={laserCode} }}, \n"
|
||||
lua += "}"
|
||||
|
||||
# Process the Target Points
|
||||
lua += """
|
||||
|
||||
-- list the target points generated by Liberation
|
||||
dcsLiberation.TargetPoints = {
|
||||
"""
|
||||
for key in luaData["TargetPoints"]:
|
||||
data = luaData["TargetPoints"][key]
|
||||
name = data["name"]
|
||||
pointType = data["type"]
|
||||
positionX = data["position"]["x"]
|
||||
positionY = data["position"]["y"]
|
||||
lua += f" {{name='{name}', pointType='{pointType}', positionX='{positionX}', positionY='{positionY}' }}, \n"
|
||||
#lua += f" {{name='{pointType} {name}', point{{x={positionX}, z={positionY} }} }}, \n"
|
||||
lua += "}"
|
||||
|
||||
lua += """
|
||||
|
||||
-- list the airbases generated by Liberation
|
||||
-- dcsLiberation.Airbases = {}
|
||||
|
||||
-- list the aircraft carriers generated by Liberation
|
||||
-- dcsLiberation.Carriers = {}
|
||||
|
||||
-- later, we'll add more data to the table
|
||||
|
||||
"""
|
||||
|
||||
|
||||
trigger = TriggerStart(comment="Set DCS Liberation data")
|
||||
trigger.add_action(DoScript(String(lua)))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
# Inject DCS-Liberation script if not done already in the plugins
|
||||
if not "dcs_liberation.lua" in listOfPluginsScripts : # don't load the script twice
|
||||
trigger = TriggerStart(comment="Load DCS Liberation script")
|
||||
fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/dcs_liberation.lua")
|
||||
trigger.add_action(DoScriptFile(fileref))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
# Inject Plugins Lua Scripts and data
|
||||
self.listOfPluginsScripts = []
|
||||
|
||||
# add a configuration for JTACAutoLase and start lasing for all JTACs
|
||||
smoke = "true"
|
||||
if hasattr(self.game.settings, "jtac_smoke_on"):
|
||||
if not self.game.settings.jtac_smoke_on:
|
||||
smoke = "false"
|
||||
|
||||
lua = """
|
||||
-- setting and starting JTACs
|
||||
env.info("DCSLiberation|: setting and starting JTACs")
|
||||
"""
|
||||
|
||||
for jtac in jtacs:
|
||||
lua += f"if dcsLiberation.JTACAutoLase then dcsLiberation.JTACAutoLase('{jtac.unit_name}', {jtac.code}, {smoke}, 'vehicle') end\n"
|
||||
|
||||
trigger = TriggerStart(comment="Start JTACs")
|
||||
trigger.add_action(DoScript(String(lua)))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
for plugin in LuaPluginManager().getPlugins():
|
||||
plugin.injectScripts(self)
|
||||
plugin.injectConfiguration(self)
|
||||
|
||||
self.assign_channels_to_flights(airgen.flights,
|
||||
airsupportgen.air_support)
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
from plugin import LuaPluginManager
|
||||
|
||||
class Settings:
|
||||
|
||||
@ -24,8 +25,6 @@ class Settings:
|
||||
self.sams = True # Legacy parameter do not use
|
||||
self.cold_start = False # Legacy parameter do not use
|
||||
self.version = None
|
||||
self.include_jtac_if_available = True
|
||||
self.jtac_smoke_on = True
|
||||
|
||||
# Performance oriented
|
||||
self.perf_red_alert_state = True
|
||||
@ -40,4 +39,21 @@ class Settings:
|
||||
self.perf_culling = False
|
||||
self.perf_culling_distance = 100
|
||||
|
||||
# LUA Plugins system
|
||||
self.plugins = {}
|
||||
for plugin in LuaPluginManager().getPlugins():
|
||||
plugin.setSettings(self)
|
||||
|
||||
|
||||
# Cheating
|
||||
self.show_red_ato = False
|
||||
|
||||
def __setstate__(self, state) -> None:
|
||||
# __setstate__ is called with the dict of the object being unpickled. We
|
||||
# can provide save compatibility for new settings options (which
|
||||
# normally would not be present in the unpickled object) by creating a
|
||||
# new settings object, updating it with the unpickled state, and
|
||||
# updating our dict with that.
|
||||
new_state = Settings().__dict__
|
||||
new_state.update(state)
|
||||
self.__dict__.update(new_state)
|
||||
|
||||
183
game/weather.py
Normal file
183
game/weather.py
Normal file
@ -0,0 +1,183 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from dcs.weather import Weather as PydcsWeather, Wind
|
||||
|
||||
from game.settings import Settings
|
||||
from theater import ConflictTheater
|
||||
|
||||
|
||||
class TimeOfDay(Enum):
|
||||
Dawn = "dawn"
|
||||
Day = "day"
|
||||
Dusk = "dusk"
|
||||
Night = "night"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WindConditions:
|
||||
at_0m: Wind
|
||||
at_2000m: Wind
|
||||
at_8000m: Wind
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Clouds:
|
||||
base: int
|
||||
density: int
|
||||
thickness: int
|
||||
precipitation: PydcsWeather.Preceptions
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Fog:
|
||||
visibility: int
|
||||
thickness: int
|
||||
|
||||
|
||||
class Weather:
|
||||
def __init__(self) -> None:
|
||||
self.clouds = self.generate_clouds()
|
||||
self.fog = self.generate_fog()
|
||||
self.wind = self.generate_wind()
|
||||
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
raise NotImplementedError
|
||||
|
||||
def generate_fog(self) -> Optional[Fog]:
|
||||
if random.randrange(5) != 0:
|
||||
return None
|
||||
return Fog(
|
||||
visibility=random.randint(2500, 5000),
|
||||
thickness=random.randint(100, 500)
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def random_wind(minimum: int, maximum) -> WindConditions:
|
||||
wind_direction = random.randint(0, 360)
|
||||
at_0m_factor = 1
|
||||
at_2000m_factor = 2
|
||||
at_8000m_factor = 3
|
||||
base_wind = random.randint(minimum, maximum)
|
||||
|
||||
return WindConditions(
|
||||
# Always some wind to make the smoke move a bit.
|
||||
at_0m=Wind(wind_direction, max(1, base_wind * at_0m_factor)),
|
||||
at_2000m=Wind(wind_direction, base_wind * at_2000m_factor),
|
||||
at_8000m=Wind(wind_direction, base_wind * at_8000m_factor)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def random_cloud_base() -> int:
|
||||
return random.randint(2000, 3000)
|
||||
|
||||
@staticmethod
|
||||
def random_cloud_thickness() -> int:
|
||||
return random.randint(100, 400)
|
||||
|
||||
|
||||
class ClearSkies(Weather):
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
return None
|
||||
|
||||
def generate_fog(self) -> Optional[Fog]:
|
||||
return None
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(0, 0)
|
||||
|
||||
|
||||
class Cloudy(Weather):
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
return Clouds(
|
||||
base=self.random_cloud_base(),
|
||||
density=random.randint(1, 8),
|
||||
thickness=self.random_cloud_thickness(),
|
||||
precipitation=PydcsWeather.Preceptions.None_
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(0, 4)
|
||||
|
||||
|
||||
class Raining(Weather):
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
return Clouds(
|
||||
base=self.random_cloud_base(),
|
||||
density=random.randint(5, 8),
|
||||
thickness=self.random_cloud_thickness(),
|
||||
precipitation=PydcsWeather.Preceptions.Rain
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(0, 6)
|
||||
|
||||
|
||||
class Thunderstorm(Weather):
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
return Clouds(
|
||||
base=self.random_cloud_base(),
|
||||
density=random.randint(9, 10),
|
||||
thickness=self.random_cloud_thickness(),
|
||||
precipitation=PydcsWeather.Preceptions.Thunderstorm
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(0, 8)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Conditions:
|
||||
time_of_day: TimeOfDay
|
||||
start_time: datetime.datetime
|
||||
weather: Weather
|
||||
|
||||
@classmethod
|
||||
def generate(cls, theater: ConflictTheater, day: datetime.date,
|
||||
time_of_day: TimeOfDay, settings: Settings) -> Conditions:
|
||||
return cls(
|
||||
time_of_day=time_of_day,
|
||||
start_time=cls.generate_start_time(
|
||||
theater, day, time_of_day, settings.night_disabled
|
||||
),
|
||||
weather=cls.generate_weather()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def generate_start_time(cls, theater: ConflictTheater, day: datetime.date,
|
||||
time_of_day: TimeOfDay,
|
||||
night_disabled: bool) -> datetime.datetime:
|
||||
if night_disabled:
|
||||
logging.info("Skip Night mission due to user settings")
|
||||
time_range = {
|
||||
TimeOfDay.Dawn: (8, 9),
|
||||
TimeOfDay.Day: (10, 12),
|
||||
TimeOfDay.Dusk: (12, 14),
|
||||
TimeOfDay.Night: (14, 17),
|
||||
}[time_of_day]
|
||||
else:
|
||||
time_range = theater.daytime_map[time_of_day.value]
|
||||
|
||||
time = datetime.time(hour=random.randint(*time_range))
|
||||
return datetime.datetime.combine(day, time)
|
||||
|
||||
@classmethod
|
||||
def generate_weather(cls) -> Weather:
|
||||
chances = {
|
||||
Thunderstorm: 1,
|
||||
Raining: 20,
|
||||
Cloudy: 60,
|
||||
ClearSkies: 20,
|
||||
}
|
||||
weather_type = random.choices(list(chances.keys()),
|
||||
weights=list(chances.values()))[0]
|
||||
return weather_type()
|
||||
@ -237,11 +237,14 @@ class FlightData:
|
||||
#: Map of radio frequencies to their assigned radio and channel, if any.
|
||||
frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment]
|
||||
|
||||
#: Data concerning the target of a CAS/Strike/SEAD flight, or None else
|
||||
targetPoint = None
|
||||
|
||||
def __init__(self, flight_type: FlightType, units: List[FlyingUnit],
|
||||
size: int, friendly: bool, departure_delay: int,
|
||||
departure: RunwayData, arrival: RunwayData,
|
||||
divert: Optional[RunwayData], waypoints: List[FlightWaypoint],
|
||||
intra_flight_channel: RadioFrequency) -> None:
|
||||
intra_flight_channel: RadioFrequency, targetPoint: Optional) -> None:
|
||||
self.flight_type = flight_type
|
||||
self.units = units
|
||||
self.size = size
|
||||
@ -254,6 +257,7 @@ class FlightData:
|
||||
self.intra_flight_channel = intra_flight_channel
|
||||
self.frequency_to_channel_map = {}
|
||||
self.callsign = create_group_callsign_from_unit(self.units[0])
|
||||
self.targetPoint = targetPoint
|
||||
|
||||
@property
|
||||
def client_units(self) -> List[FlyingUnit]:
|
||||
@ -415,6 +419,7 @@ class SCR522RadioChannelAllocator(RadioChannelAllocator):
|
||||
|
||||
# TODO : Some GCI on Channel 4 ?
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AircraftData:
|
||||
"""Additional aircraft data not exposed by pydcs."""
|
||||
@ -438,7 +443,9 @@ class AircraftData:
|
||||
AIRCRAFT_DATA: Dict[str, AircraftData] = {
|
||||
"A-10C": AircraftData(
|
||||
inter_flight_radio=get_radio("AN/ARC-164"),
|
||||
intra_flight_radio=get_radio("AN/ARC-164"), # VHF for intraflight is not accepted anymore by DCS (see https://forums.eagle.ru/showthread.php?p=4499738)
|
||||
# VHF for intraflight is not accepted anymore by DCS
|
||||
# (see https://forums.eagle.ru/showthread.php?p=4499738).
|
||||
intra_flight_radio=get_radio("AN/ARC-164"),
|
||||
channel_allocator=WarthogRadioChannelAllocator()
|
||||
),
|
||||
|
||||
@ -528,6 +535,7 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = {
|
||||
channel_namer=SCR522ChannelNamer
|
||||
),
|
||||
}
|
||||
AIRCRAFT_DATA["A-10C_2"] = AIRCRAFT_DATA["A-10C"]
|
||||
AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"]
|
||||
AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
|
||||
|
||||
@ -641,7 +649,8 @@ class AircraftConflictGenerator:
|
||||
divert=None,
|
||||
# Waypoints are added later, after they've had their TOTs set.
|
||||
waypoints=[],
|
||||
intra_flight_channel=channel
|
||||
intra_flight_channel=channel,
|
||||
targetPoint=flight.targetPoint,
|
||||
))
|
||||
|
||||
# Special case so Su 33 carrier take off
|
||||
@ -788,6 +797,8 @@ class AircraftConflictGenerator:
|
||||
self.clear_parking_slots()
|
||||
|
||||
for package in ato.packages:
|
||||
if not package.flights:
|
||||
continue
|
||||
timing = PackageWaypointTiming.for_package(package)
|
||||
for flight in package.flights:
|
||||
culled = self.game.position_culled(flight.from_cp.position)
|
||||
@ -991,7 +1002,7 @@ class AircraftConflictGenerator:
|
||||
flight: Flight, timing: PackageWaypointTiming,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
flight_type = flight.flight_type
|
||||
if flight_type in [FlightType.CAP, FlightType.BARCAP, FlightType.TARCAP,
|
||||
if flight_type in [FlightType.BARCAP, FlightType.TARCAP,
|
||||
FlightType.INTERCEPTION]:
|
||||
self.configure_cap(group, flight, dynamic_runways)
|
||||
elif flight_type in [FlightType.CAS, FlightType.BAI]:
|
||||
@ -1129,8 +1140,9 @@ class HoldPointBuilder(PydcsWaypointBuilder):
|
||||
altitude=waypoint.alt,
|
||||
pattern=OrbitAction.OrbitPattern.Circle
|
||||
))
|
||||
loiter.stop_after_time(
|
||||
self.timing.push_time(self.flight, waypoint.position))
|
||||
push_time = self.timing.push_time(self.flight, self.waypoint)
|
||||
self.waypoint.departure_time = push_time
|
||||
loiter.stop_after_time(push_time)
|
||||
waypoint.add_task(loiter)
|
||||
return waypoint
|
||||
|
||||
|
||||
@ -30,6 +30,7 @@ AWACS_ALT = 13000
|
||||
@dataclass
|
||||
class AwacsInfo:
|
||||
"""AWACS information for the kneeboard."""
|
||||
dcsGroupName: str
|
||||
callsign: str
|
||||
freq: RadioFrequency
|
||||
|
||||
@ -37,6 +38,7 @@ class AwacsInfo:
|
||||
@dataclass
|
||||
class TankerInfo:
|
||||
"""Tanker information for the kneeboard."""
|
||||
dcsGroupName: str
|
||||
callsign: str
|
||||
variant: str
|
||||
freq: RadioFrequency
|
||||
@ -116,7 +118,7 @@ class AirSupportConflictGenerator:
|
||||
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
|
||||
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
|
||||
|
||||
self.air_support.tankers.append(TankerInfo(callsign, variant, freq, tacan))
|
||||
self.air_support.tankers.append(TankerInfo(str(tanker_group.name), callsign, variant, freq, tacan))
|
||||
|
||||
if is_awacs_enabled:
|
||||
try:
|
||||
@ -138,6 +140,6 @@ class AirSupportConflictGenerator:
|
||||
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
|
||||
|
||||
self.air_support.awacs.append(AwacsInfo(
|
||||
callsign_for_support_unit(awacs_flight), freq))
|
||||
str(awacs_flight.name), callsign_for_support_unit(awacs_flight), freq))
|
||||
except:
|
||||
print("No AWACS for faction")
|
||||
@ -34,6 +34,7 @@ from gen.ground_forces.ai_ground_planner import (
|
||||
from .callsigns import callsign_for_support_unit
|
||||
from .conflictgen import Conflict
|
||||
from .ground_forces.combat_stance import CombatStance
|
||||
from plugin import LuaPluginManager
|
||||
|
||||
SPREAD_DISTANCE_FACTOR = 0.1, 0.3
|
||||
SPREAD_DISTANCE_SIZE_FACTOR = 0.1
|
||||
@ -54,6 +55,7 @@ RANDOM_OFFSET_ATTACK = 250
|
||||
@dataclass(frozen=True)
|
||||
class JtacInfo:
|
||||
"""JTAC information."""
|
||||
dcsGroupName: str
|
||||
unit_name: str
|
||||
callsign: str
|
||||
region: str
|
||||
@ -138,7 +140,9 @@ class GroundConflictGenerator:
|
||||
self.plan_action_for_groups(self.enemy_stance, enemy_groups, player_groups, self.conflict.heading - 90, self.conflict.to_cp, self.conflict.from_cp)
|
||||
|
||||
# Add JTAC
|
||||
if "has_jtac" in self.game.player_faction and self.game.player_faction["has_jtac"] and self.game.settings.include_jtac_if_available:
|
||||
jtacPlugin = LuaPluginManager().getPlugin("jtacautolase")
|
||||
useJTAC = jtacPlugin and jtacPlugin.isEnabled()
|
||||
if "has_jtac" in self.game.player_faction and self.game.player_faction["has_jtac"] and useJTAC:
|
||||
n = "JTAC" + str(self.conflict.from_cp.id) + str(self.conflict.to_cp.id)
|
||||
code = 1688 - len(self.jtacs)
|
||||
|
||||
@ -158,7 +162,7 @@ class GroundConflictGenerator:
|
||||
frontline = f"Frontline {self.conflict.from_cp.name}/{self.conflict.to_cp.name}"
|
||||
# Note: Will need to change if we ever add ground based JTAC.
|
||||
callsign = callsign_for_support_unit(jtac)
|
||||
self.jtacs.append(JtacInfo(n, callsign, frontline, str(code)))
|
||||
self.jtacs.append(JtacInfo(str(jtac.name), n, callsign, frontline, str(code)))
|
||||
|
||||
def gen_infantry_group_for_group(self, group, is_player, side:Country, forward_heading):
|
||||
|
||||
|
||||
@ -129,6 +129,11 @@ class BriefingGenerator(MissionInfoGenerator):
|
||||
self.description += "DCS Liberation turn #" + str(self.game.turn) + "\n"
|
||||
self.description += "=" * 15 + "\n\n"
|
||||
|
||||
self.description += (
|
||||
"Most briefing information, including communications and flight "
|
||||
"plan information, can be found on your kneeboard.\n\n"
|
||||
)
|
||||
|
||||
self.generate_ongoing_war_text()
|
||||
|
||||
self.description += "\n"*2
|
||||
|
||||
@ -1,147 +1,36 @@
|
||||
import logging
|
||||
import random
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
|
||||
from dcs.mission import Mission
|
||||
from dcs.weather import Weather, Wind
|
||||
|
||||
from .conflictgen import Conflict
|
||||
|
||||
WEATHER_CLOUD_BASE = 2000, 3000
|
||||
WEATHER_CLOUD_DENSITY = 1, 8
|
||||
WEATHER_CLOUD_THICKNESS = 100, 400
|
||||
WEATHER_CLOUD_BASE_MIN = 1600
|
||||
|
||||
WEATHER_FOG_CHANCE = 20
|
||||
WEATHER_FOG_VISIBILITY = 2500, 5000
|
||||
WEATHER_FOG_THICKNESS = 100, 500
|
||||
|
||||
RANDOM_TIME = {
|
||||
"night": 7,
|
||||
"dusk": 40,
|
||||
"dawn": 40,
|
||||
"day": 100,
|
||||
}
|
||||
|
||||
RANDOM_WEATHER = {
|
||||
1: 0, # thunderstorm
|
||||
2: 20, # rain
|
||||
3: 80, # clouds
|
||||
4: 100, # clear
|
||||
}
|
||||
from game.weather import Clouds, Fog, Conditions, WindConditions
|
||||
|
||||
|
||||
class EnvironmentSettings:
|
||||
weather_dict = None
|
||||
start_time = None
|
||||
|
||||
|
||||
class EnviromentGenerator:
|
||||
def __init__(self, mission: Mission, conflict: Conflict, game):
|
||||
class EnvironmentGenerator:
|
||||
def __init__(self, mission: Mission, conditions: Conditions) -> None:
|
||||
self.mission = mission
|
||||
self.conflict = conflict
|
||||
self.game = game
|
||||
self.conditions = conditions
|
||||
|
||||
def _gen_time(self):
|
||||
def set_clouds(self, clouds: Optional[Clouds]) -> None:
|
||||
if clouds is None:
|
||||
return
|
||||
self.mission.weather.clouds_base = clouds.base
|
||||
self.mission.weather.clouds_thickness = clouds.thickness
|
||||
self.mission.weather.clouds_density = clouds.density
|
||||
self.mission.weather.clouds_iprecptns = clouds.precipitation
|
||||
|
||||
start_time = self.game.current_day
|
||||
def set_fog(self, fog: Optional[Fog]) -> None:
|
||||
if fog is None:
|
||||
return
|
||||
self.mission.weather.fog_visibility = fog.visibility
|
||||
self.mission.weather.fog_thickness = fog.thickness
|
||||
|
||||
daytime = self.game.current_turn_daytime
|
||||
logging.info("Mission time will be {}".format(daytime))
|
||||
if self.game.settings.night_disabled:
|
||||
logging.info("Skip Night mission due to user settings")
|
||||
if daytime == "dawn":
|
||||
time_range = (8, 9)
|
||||
elif daytime == "day":
|
||||
time_range = (10, 12)
|
||||
elif daytime == "dusk":
|
||||
time_range = (12, 14)
|
||||
elif daytime == "night":
|
||||
time_range = (14, 17)
|
||||
else:
|
||||
time_range = (10, 12)
|
||||
else:
|
||||
time_range = self.game.theater.daytime_map[daytime]
|
||||
|
||||
start_time += timedelta(hours=random.randint(*time_range))
|
||||
|
||||
logging.info("time - {}, slot - {}, night skipped - {}".format(
|
||||
str(start_time),
|
||||
str(time_range),
|
||||
self.game.settings.night_disabled))
|
||||
|
||||
self.mission.start_time = start_time
|
||||
|
||||
def _generate_wind(self, wind_speed, wind_direction=None):
|
||||
# wind
|
||||
if not wind_direction:
|
||||
wind_direction = random.randint(0, 360)
|
||||
|
||||
self.mission.weather.wind_at_ground = Wind(wind_direction, wind_speed)
|
||||
self.mission.weather.wind_at_2000 = Wind(wind_direction, wind_speed * 2)
|
||||
self.mission.weather.wind_at_8000 = Wind(wind_direction, wind_speed * 3)
|
||||
|
||||
def _generate_base_weather(self):
|
||||
# clouds
|
||||
self.mission.weather.clouds_base = random.randint(*WEATHER_CLOUD_BASE)
|
||||
self.mission.weather.clouds_density = random.randint(*WEATHER_CLOUD_DENSITY)
|
||||
self.mission.weather.clouds_thickness = random.randint(*WEATHER_CLOUD_THICKNESS)
|
||||
|
||||
# wind
|
||||
self._generate_wind(random.randint(0, 4))
|
||||
|
||||
# fog
|
||||
if random.randint(0, 100) < WEATHER_FOG_CHANCE:
|
||||
self.mission.weather.fog_visibility = random.randint(*WEATHER_FOG_VISIBILITY)
|
||||
self.mission.weather.fog_thickness = random.randint(*WEATHER_FOG_THICKNESS)
|
||||
|
||||
def _gen_random_weather(self):
|
||||
weather_type = None
|
||||
for k, v in RANDOM_WEATHER.items():
|
||||
if random.randint(0, 100) <= v:
|
||||
weather_type = k
|
||||
break
|
||||
|
||||
logging.info("generated weather {}".format(weather_type))
|
||||
if weather_type == 1:
|
||||
# thunderstorm
|
||||
self._generate_base_weather()
|
||||
self._generate_wind(random.randint(0, 8))
|
||||
|
||||
self.mission.weather.clouds_density = random.randint(9, 10)
|
||||
self.mission.weather.clouds_iprecptns = Weather.Preceptions.Thunderstorm
|
||||
elif weather_type == 2:
|
||||
# rain
|
||||
self._generate_base_weather()
|
||||
self.mission.weather.clouds_density = random.randint(5, 8)
|
||||
self.mission.weather.clouds_iprecptns = Weather.Preceptions.Rain
|
||||
|
||||
self._generate_wind(random.randint(0, 6))
|
||||
elif weather_type == 3:
|
||||
# clouds
|
||||
self._generate_base_weather()
|
||||
elif weather_type == 4:
|
||||
# clear
|
||||
pass
|
||||
|
||||
if self.mission.weather.clouds_density > 0:
|
||||
# sometimes clouds are randomized way too low and need to be fixed
|
||||
self.mission.weather.clouds_base = max(self.mission.weather.clouds_base, WEATHER_CLOUD_BASE_MIN)
|
||||
|
||||
if self.mission.weather.wind_at_ground.speed == 0:
|
||||
# frontline smokes look silly w/o any wind
|
||||
self._generate_wind(1)
|
||||
|
||||
def generate(self) -> EnvironmentSettings:
|
||||
self._gen_time()
|
||||
self._gen_random_weather()
|
||||
|
||||
settings = EnvironmentSettings()
|
||||
settings.start_time = self.mission.start_time
|
||||
settings.weather_dict = self.mission.weather.dict()
|
||||
return settings
|
||||
|
||||
def load(self, settings: EnvironmentSettings):
|
||||
self.mission.start_time = settings.start_time
|
||||
self.mission.weather.load_from_dict(settings.weather_dict)
|
||||
def set_wind(self, wind: WindConditions) -> None:
|
||||
self.mission.weather.wind_at_ground = wind.at_0m
|
||||
self.mission.weather.wind_at_2000 = wind.at_2000m
|
||||
self.mission.weather.wind_at_8000 = wind.at_8000m
|
||||
|
||||
def generate(self):
|
||||
self.mission.start_time = self.conditions.start_time
|
||||
self.set_clouds(self.conditions.weather.clouds)
|
||||
self.set_fog(self.conditions.weather.fog)
|
||||
self.set_wind(self.conditions.weather.wind)
|
||||
|
||||
@ -131,7 +131,7 @@ class AircraftAllocator:
|
||||
|
||||
@staticmethod
|
||||
def preferred_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
cap_missions = (FlightType.BARCAP, FlightType.CAP, FlightType.TARCAP)
|
||||
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
|
||||
if task in cap_missions:
|
||||
return CAP_PREFERRED
|
||||
elif task == FlightType.CAS:
|
||||
@ -147,7 +147,7 @@ class AircraftAllocator:
|
||||
|
||||
@staticmethod
|
||||
def capable_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
cap_missions = (FlightType.BARCAP, FlightType.CAP, FlightType.TARCAP)
|
||||
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
|
||||
if task in cap_missions:
|
||||
return CAP_CAPABLE
|
||||
elif task == FlightType.CAS:
|
||||
@ -209,6 +209,7 @@ class PackageBuilder:
|
||||
flight = Flight(aircraft, plan.num_aircraft, airfield, plan.task,
|
||||
self.start_type)
|
||||
self.package.add_flight(flight)
|
||||
flight.targetPoint = self.package.target
|
||||
return True
|
||||
|
||||
def build(self) -> Package:
|
||||
@ -221,7 +222,7 @@ class PackageBuilder:
|
||||
for flight in flights:
|
||||
self.global_inventory.return_from_flight(flight)
|
||||
self.package.remove_flight(flight)
|
||||
|
||||
flight.targetPoint = None
|
||||
|
||||
class ObjectiveFinder:
|
||||
"""Identifies potential objectives for the mission planner."""
|
||||
@ -389,9 +390,6 @@ class CoalitionMissionPlanner:
|
||||
MAX_SEAD_RANGE = nm_to_meter(150)
|
||||
MAX_STRIKE_RANGE = nm_to_meter(150)
|
||||
|
||||
NON_CAP_MIN_DELAY = 1
|
||||
NON_CAP_MAX_DELAY = 5
|
||||
|
||||
def __init__(self, game: Game, is_player: bool) -> None:
|
||||
self.game = game
|
||||
self.is_player = is_player
|
||||
@ -403,7 +401,7 @@ class CoalitionMissionPlanner:
|
||||
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
|
||||
for cp in self.objective_finder.vulnerable_control_points():
|
||||
yield ProposedMission(cp, [
|
||||
ProposedFlight(FlightType.CAP, 2, self.MAX_CAP_RANGE),
|
||||
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
|
||||
])
|
||||
|
||||
# Find front lines, plan CAP.
|
||||
@ -492,11 +490,7 @@ class CoalitionMissionPlanner:
|
||||
error = random.randint(-margin, margin)
|
||||
yield max(0, time + error)
|
||||
|
||||
dca_types = (
|
||||
FlightType.BARCAP,
|
||||
FlightType.CAP,
|
||||
FlightType.INTERCEPTION,
|
||||
)
|
||||
dca_types = (FlightType.BARCAP, FlightType.INTERCEPTION)
|
||||
|
||||
non_dca_packages = [p for p in self.ato.packages if
|
||||
p.primary_task not in dca_types]
|
||||
|
||||
@ -326,7 +326,7 @@ SEAD_CAPABLE = [
|
||||
F_4E,
|
||||
FA_18C_hornet,
|
||||
F_15E,
|
||||
# F_16C_50, Not yet
|
||||
F_16C_50,
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
from enum import Enum
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, Iterable, List, Optional
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
from game import db
|
||||
from dcs.unittype import UnitType
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
from theater.controlpoint import ControlPoint, MissionTarget
|
||||
|
||||
|
||||
class FlightType(Enum):
|
||||
CAP = 0
|
||||
CAP = 0 # Do not use. Use BARCAP or TARCAP.
|
||||
TARCAP = 1
|
||||
BARCAP = 2
|
||||
CAS = 3
|
||||
@ -91,17 +93,22 @@ class FlightWaypoint:
|
||||
self.only_for_player = False
|
||||
self.data = None
|
||||
|
||||
# This is set very late by the air conflict generator (part of mission
|
||||
# These are set very late by the air conflict generator (part of mission
|
||||
# generation). We do it late so that we don't need to propagate changes
|
||||
# to waypoint times whenever the player alters the package TOT or the
|
||||
# flight's offset in the UI.
|
||||
self.tot: Optional[int] = None
|
||||
self.departure_time: Optional[int] = None
|
||||
|
||||
@property
|
||||
def position(self) -> Point:
|
||||
return Point(self.x, self.y)
|
||||
|
||||
@classmethod
|
||||
def from_pydcs(cls, point: MovingPoint,
|
||||
from_cp: ControlPoint) -> "FlightWaypoint":
|
||||
waypoint = FlightWaypoint(point.position.x, point.position.y,
|
||||
point.alt)
|
||||
waypoint = FlightWaypoint(FlightWaypointType.NAV, point.position.x,
|
||||
point.position.y, point.alt)
|
||||
waypoint.alt_type = point.alt_type
|
||||
# Other actions exist... but none of them *should* be the first
|
||||
# waypoint for a flight.
|
||||
@ -130,6 +137,7 @@ class Flight:
|
||||
use_custom_loadout = False
|
||||
preset_loadout_name = ""
|
||||
group = False # Contains DCS Mission group data after mission has been generated
|
||||
targetPoint = None # Contains either None or a Strike/SEAD target point location
|
||||
|
||||
def __init__(self, unit_type: UnitType, count: int, from_cp: ControlPoint,
|
||||
flight_type: FlightType, start_type: str) -> None:
|
||||
@ -151,13 +159,10 @@ class Flight:
|
||||
return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \
|
||||
+ " (" + str(len(self.points)) + " wpt)"
|
||||
|
||||
|
||||
# Test
|
||||
if __name__ == '__main__':
|
||||
from dcs.planes import A_10C
|
||||
from theater import ControlPoint, Point, List
|
||||
|
||||
from_cp = ControlPoint(0, "AA", Point(0, 0), Point(0, 0), [], 0, 0)
|
||||
f = Flight(A_10C(), 4, from_cp, FlightType.CAS, "Cold")
|
||||
f.scheduled_in = 50
|
||||
print(f)
|
||||
def waypoint_with_type(
|
||||
self,
|
||||
types: Iterable[FlightWaypointType]) -> Optional[FlightWaypoint]:
|
||||
for waypoint in self.points:
|
||||
if waypoint.waypoint_type in types:
|
||||
return waypoint
|
||||
return None
|
||||
|
||||
@ -69,8 +69,6 @@ class FlightPlanBuilder:
|
||||
logging.error("BAI flight plan generation not implemented")
|
||||
elif task == FlightType.BARCAP:
|
||||
self.generate_barcap(flight)
|
||||
elif task == FlightType.CAP:
|
||||
self.generate_barcap(flight)
|
||||
elif task == FlightType.CAS:
|
||||
self.generate_cas(flight)
|
||||
elif task == FlightType.DEAD:
|
||||
@ -103,8 +101,10 @@ class FlightPlanBuilder:
|
||||
logging.error(
|
||||
"Troop transport flight plan generation not implemented"
|
||||
)
|
||||
except InvalidObjectiveLocation as ex:
|
||||
logging.error(f"Could not create flight plan: {ex}")
|
||||
else:
|
||||
logging.error(f"Unsupported task type: {task.name}")
|
||||
except InvalidObjectiveLocation:
|
||||
logging.exception(f"Could not create flight plan")
|
||||
|
||||
def regenerate_package_waypoints(self) -> None:
|
||||
ingress_point = self._ingress_point()
|
||||
@ -187,6 +187,10 @@ class FlightPlanBuilder:
|
||||
|
||||
closest_cache = ObjectiveDistanceCache.get_closest_airfields(location)
|
||||
for airfield in closest_cache.closest_airfields:
|
||||
# If the mission is a BARCAP of an enemy airfield, find the *next*
|
||||
# closest enemy airfield.
|
||||
if airfield == self.package.target:
|
||||
continue
|
||||
if airfield.captured != self.is_player:
|
||||
closest_airfield = airfield
|
||||
break
|
||||
@ -198,10 +202,19 @@ class FlightPlanBuilder:
|
||||
closest_airfield.position
|
||||
)
|
||||
|
||||
min_distance_from_enemy = nm_to_meter(20)
|
||||
distance_to_airfield = int(closest_airfield.position.distance_to_point(
|
||||
self.package.target.position
|
||||
))
|
||||
distance_to_no_fly = distance_to_airfield - min_distance_from_enemy
|
||||
min_cap_distance = min(self.doctrine.cap_min_distance_from_cp,
|
||||
distance_to_no_fly)
|
||||
max_cap_distance = min(self.doctrine.cap_max_distance_from_cp,
|
||||
distance_to_no_fly)
|
||||
|
||||
end = location.position.point_from_heading(
|
||||
heading,
|
||||
random.randint(self.doctrine.cap_min_distance_from_cp,
|
||||
self.doctrine.cap_max_distance_from_cp)
|
||||
random.randint(min_cap_distance, max_cap_distance)
|
||||
)
|
||||
diameter = random.randint(
|
||||
self.doctrine.cap_min_track_length,
|
||||
@ -424,7 +437,6 @@ class FlightPlanBuilder:
|
||||
def _heading_to_package_airfield(self, point: Point) -> int:
|
||||
return self.package_airfield().position.heading_between_point(point)
|
||||
|
||||
# TODO: Set ingress/egress/join/split points in the Package.
|
||||
def package_airfield(self) -> ControlPoint:
|
||||
# We'll always have a package, but if this is being planned via the UI
|
||||
# it could be the first flight in the package.
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable, Optional
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.utils import meter_to_nm
|
||||
from gen.ato import Package
|
||||
@ -17,25 +19,101 @@ from gen.flights.flight import (
|
||||
|
||||
|
||||
CAP_DURATION = 30 # Minutes
|
||||
CAP_TYPES = (FlightType.BARCAP, FlightType.CAP)
|
||||
|
||||
INGRESS_TYPES = {
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
}
|
||||
|
||||
IP_TYPES = {
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
}
|
||||
|
||||
|
||||
class GroundSpeed:
|
||||
@classmethod
|
||||
def for_package(cls, package: Package) -> int:
|
||||
speeds = []
|
||||
for flight in package.flights:
|
||||
speeds.append(cls.for_flight(flight))
|
||||
return min(speeds) # knots
|
||||
|
||||
@staticmethod
|
||||
def for_flight(_flight: Flight) -> int:
|
||||
# TODO: Gather data so this is useful.
|
||||
def mission_speed(package: Package) -> int:
|
||||
speeds = set()
|
||||
for flight in package.flights:
|
||||
waypoint = flight.waypoint_with_type(IP_TYPES)
|
||||
if waypoint is None:
|
||||
logging.error(f"Could not find ingress point for {flight}.")
|
||||
if flight.points:
|
||||
logging.warning(
|
||||
"Using first waypoint for mission altitude.")
|
||||
waypoint = flight.points[0]
|
||||
else:
|
||||
logging.warning(
|
||||
"Flight has no waypoints. Assuming mission altitude "
|
||||
"of 25000 feet.")
|
||||
waypoint = FlightWaypoint(FlightWaypointType.NAV, 0, 0,
|
||||
25000)
|
||||
speeds.add(GroundSpeed.for_flight(flight, waypoint.alt))
|
||||
return min(speeds)
|
||||
|
||||
@classmethod
|
||||
def for_flight(cls, flight: Flight, altitude: int) -> int:
|
||||
if not issubclass(flight.unit_type, FlyingType):
|
||||
raise TypeError("Flight has non-flying unit")
|
||||
|
||||
# TODO: Expose both a cruise speed and target speed.
|
||||
# The cruise speed can be used for ascent, hold, join, and RTB to save
|
||||
# on fuel, but mission speed will be fast enough to keep the flight
|
||||
# safer.
|
||||
return 400 # knots
|
||||
|
||||
c_sound_sea_level = 661.5
|
||||
|
||||
# DCS's max speed is in kph at 0 MSL. Convert to knots.
|
||||
max_speed = flight.unit_type.max_speed * 0.539957
|
||||
if max_speed > c_sound_sea_level:
|
||||
# Aircraft is supersonic. Limit to mach 0.8 to conserve fuel and
|
||||
# account for heavily loaded jets.
|
||||
return int(cls.from_mach(0.8, altitude))
|
||||
|
||||
# For subsonic aircraft, assume the aircraft can reasonably perform at
|
||||
# 80% of its maximum, and that it can maintain the same mach at altitude
|
||||
# as it can at sea level. This probably isn't great assumption, but
|
||||
# might. be sufficient given the wiggle room. We can come up with
|
||||
# another heuristic if needed.
|
||||
mach = max_speed * 0.8 / c_sound_sea_level
|
||||
return int(cls.from_mach(mach, altitude)) # knots
|
||||
|
||||
@staticmethod
|
||||
def from_mach(mach: float, altitude: int) -> float:
|
||||
"""Returns the ground speed in knots for the given mach and altitude.
|
||||
|
||||
Args:
|
||||
mach: The mach number to convert to ground speed.
|
||||
altitude: The altitude in feet.
|
||||
|
||||
Returns:
|
||||
The ground speed corresponding to the given altitude and mach number
|
||||
in knots.
|
||||
"""
|
||||
# https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html
|
||||
if altitude <= 36152:
|
||||
temperature_f = 59 - 0.00356 * altitude
|
||||
else:
|
||||
# There's another formula for altitudes over 82k feet, but we better
|
||||
# not be planning waypoints that high...
|
||||
temperature_f = -70
|
||||
|
||||
temperature_k = (temperature_f + 459.67) * (5 / 9)
|
||||
|
||||
# https://www.engineeringtoolbox.com/specific-heat-ratio-d_602.html
|
||||
# Dependent on temperature, but varies very little (+/-0.001)
|
||||
# between -40F and 180F.
|
||||
heat_capacity_ratio = 1.4
|
||||
|
||||
# https://www.grc.nasa.gov/WWW/K-12/airplane/sound.html
|
||||
gas_constant = 286 # m^2/s^2/K
|
||||
c_sound = math.sqrt(heat_capacity_ratio * gas_constant * temperature_k)
|
||||
# c_sound is in m/s, convert to knots.
|
||||
return (c_sound * 1.944) * mach
|
||||
|
||||
|
||||
class TravelTime:
|
||||
@ -72,7 +150,7 @@ class TotEstimator:
|
||||
# Takeoff immediately.
|
||||
return 0
|
||||
|
||||
if self.package.primary_task in CAP_TYPES:
|
||||
if self.package.primary_task == FlightType.BARCAP:
|
||||
start_time = self.timing.race_track_start
|
||||
else:
|
||||
start_time = self.timing.join
|
||||
@ -97,13 +175,7 @@ class TotEstimator:
|
||||
The earliest possible TOT for the given flight in seconds. Returns 0
|
||||
if an ingress point cannot be found.
|
||||
"""
|
||||
stop_types = {
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
}
|
||||
time_to_ingress = self.estimate_waypoints_to_target(flight, stop_types)
|
||||
time_to_ingress = self.estimate_waypoints_to_target(flight, IP_TYPES)
|
||||
if time_to_ingress is None:
|
||||
logging.warning(
|
||||
f"Found no ingress types. Cannot estimate TOT for {flight}")
|
||||
@ -111,7 +183,7 @@ class TotEstimator:
|
||||
# the package.
|
||||
return 0
|
||||
|
||||
if self.package.primary_task in CAP_TYPES:
|
||||
if self.package.primary_task == FlightType.BARCAP:
|
||||
# The racetrack start *is* the target. The package target is the
|
||||
# protected objective.
|
||||
time_to_target = 0
|
||||
@ -119,7 +191,7 @@ class TotEstimator:
|
||||
assert self.package.waypoints is not None
|
||||
time_to_target = TravelTime.between_points(
|
||||
self.package.waypoints.ingress, self.package.target.position,
|
||||
GroundSpeed.for_package(self.package))
|
||||
GroundSpeed.mission_speed(self.package))
|
||||
return sum([
|
||||
self.estimate_startup(flight),
|
||||
self.estimate_ground_ops(flight),
|
||||
@ -146,30 +218,38 @@ class TotEstimator:
|
||||
self, flight: Flight,
|
||||
stop_types: Iterable[FlightWaypointType]) -> Optional[int]:
|
||||
total = 0
|
||||
# TODO: This is AGL. We want MSL.
|
||||
previous_altitude = 0
|
||||
previous_position = flight.from_cp.position
|
||||
for waypoint in flight.points:
|
||||
position = Point(waypoint.x, waypoint.y)
|
||||
total += TravelTime.between_points(
|
||||
previous_position, position,
|
||||
self.speed_to_waypoint(flight, waypoint)
|
||||
self.speed_to_waypoint(flight, waypoint, previous_altitude)
|
||||
)
|
||||
previous_position = position
|
||||
previous_altitude = waypoint.alt
|
||||
if waypoint.waypoint_type in stop_types:
|
||||
return total
|
||||
|
||||
return None
|
||||
|
||||
def speed_to_waypoint(self, flight: Flight,
|
||||
waypoint: FlightWaypoint) -> int:
|
||||
def speed_to_waypoint(self, flight: Flight, waypoint: FlightWaypoint,
|
||||
from_altitude: int) -> int:
|
||||
# TODO: Adjust if AGL.
|
||||
# We don't have an exact heightmap, but we should probably be performing
|
||||
# *some* adjustment for NTTR since the minimum altitude of the map is
|
||||
# near 2000 ft MSL.
|
||||
alt_for_speed = min(from_altitude, waypoint.alt)
|
||||
pre_join = (FlightWaypointType.LOITER, FlightWaypointType.JOIN)
|
||||
if waypoint.waypoint_type == FlightWaypointType.ASCEND_POINT:
|
||||
# Flights that start airborne already have some altitude and a good
|
||||
# amount of speed.
|
||||
factor = 1.0 if flight.start_type == "In Flight" else 0.5
|
||||
return int(GroundSpeed.for_flight(flight) * factor)
|
||||
return int(GroundSpeed.for_flight(flight, alt_for_speed) * factor)
|
||||
elif waypoint.waypoint_type in pre_join:
|
||||
return GroundSpeed.for_flight(flight)
|
||||
return GroundSpeed.for_package(self.package)
|
||||
return GroundSpeed.for_flight(flight, alt_for_speed)
|
||||
return GroundSpeed.mission_speed(self.package)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -197,24 +277,24 @@ class PackageWaypointTiming:
|
||||
|
||||
@property
|
||||
def race_track_start(self) -> int:
|
||||
if self.package.primary_task in CAP_TYPES:
|
||||
if self.package.primary_task == FlightType.BARCAP:
|
||||
return self.package.time_over_target
|
||||
else:
|
||||
return self.ingress
|
||||
|
||||
@property
|
||||
def race_track_end(self) -> int:
|
||||
if self.package.primary_task in CAP_TYPES:
|
||||
if self.package.primary_task == FlightType.BARCAP:
|
||||
return self.target + CAP_DURATION * 60
|
||||
else:
|
||||
return self.egress
|
||||
|
||||
def push_time(self, flight: Flight, hold_point: Point) -> int:
|
||||
def push_time(self, flight: Flight, hold_point: FlightWaypoint) -> int:
|
||||
assert self.package.waypoints is not None
|
||||
return self.join - TravelTime.between_points(
|
||||
hold_point,
|
||||
Point(hold_point.x, hold_point.y),
|
||||
self.package.waypoints.join,
|
||||
GroundSpeed.for_flight(flight)
|
||||
GroundSpeed.for_flight(flight, hold_point.alt)
|
||||
)
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[int]:
|
||||
@ -224,15 +304,9 @@ class PackageWaypointTiming:
|
||||
FlightWaypointType.TARGET_SHIP,
|
||||
)
|
||||
|
||||
ingress_types = (
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
)
|
||||
|
||||
if waypoint.waypoint_type == FlightWaypointType.JOIN:
|
||||
return self.join
|
||||
elif waypoint.waypoint_type in ingress_types:
|
||||
elif waypoint.waypoint_type in INGRESS_TYPES:
|
||||
return self.ingress
|
||||
elif waypoint.waypoint_type in target_types:
|
||||
return self.target
|
||||
@ -247,7 +321,7 @@ class PackageWaypointTiming:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint,
|
||||
flight: Flight) -> Optional[int]:
|
||||
if waypoint.waypoint_type == FlightWaypointType.LOITER:
|
||||
return self.push_time(flight, Point(waypoint.x, waypoint.y))
|
||||
return self.push_time(flight, waypoint)
|
||||
elif waypoint.waypoint_type == FlightWaypointType.PATROL:
|
||||
return self.race_track_end
|
||||
return None
|
||||
@ -256,7 +330,17 @@ class PackageWaypointTiming:
|
||||
def for_package(cls, package: Package) -> PackageWaypointTiming:
|
||||
assert package.waypoints is not None
|
||||
|
||||
group_ground_speed = GroundSpeed.for_package(package)
|
||||
# TODO: Plan similar altitudes for the in-country leg of the mission.
|
||||
# Waypoint altitudes for a given flight *shouldn't* differ too much
|
||||
# between the join and split points, so we don't need speeds for each
|
||||
# leg individually since they should all be fairly similar. This doesn't
|
||||
# hold too well right now since nothing is stopping each waypoint from
|
||||
# jumping 20k feet each time, but that's a huge waste of energy we
|
||||
# should be avoiding anyway.
|
||||
if not package.flights:
|
||||
raise ValueError("Cannot plan TOT for package with no flights")
|
||||
|
||||
group_ground_speed = GroundSpeed.mission_speed(package)
|
||||
|
||||
ingress = package.time_over_target - TravelTime.between_points(
|
||||
package.waypoints.ingress,
|
||||
|
||||
@ -267,12 +267,6 @@ class WaypointBuilder:
|
||||
waypoint.pretty_name = "Race-track start"
|
||||
self.waypoints.append(waypoint)
|
||||
|
||||
# TODO: Does this actually do anything?
|
||||
# orbit0.targets.append(location)
|
||||
# Note: Targets of PATROL TRACK waypoints are the points to be defended.
|
||||
# orbit0.targets.append(flight.from_cp)
|
||||
# orbit0.targets.append(center)
|
||||
|
||||
def race_track_end(self, position: Point, altitude: int) -> None:
|
||||
"""Creates a racetrack end waypoint.
|
||||
|
||||
|
||||
@ -29,16 +29,19 @@ from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from dcs.mapping import Point
|
||||
from dcs.mission import Mission
|
||||
from dcs.unittype import FlyingType
|
||||
from tabulate import tabulate
|
||||
|
||||
from game.utils import meter_to_nm
|
||||
from . import units
|
||||
from .aircraft import AIRCRAFT_DATA, FlightData
|
||||
from .airfields import RunwayData
|
||||
from .airsupportgen import AwacsInfo, TankerInfo
|
||||
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
|
||||
from .flights.flight import FlightWaypoint, FlightWaypointType
|
||||
from .flights.traveltime import TravelTime
|
||||
from .radios import RadioFrequency
|
||||
|
||||
|
||||
@ -111,6 +114,7 @@ class FlightPlanBuilder:
|
||||
self.start_time = start_time
|
||||
self.rows: List[List[str]] = []
|
||||
self.target_points: List[NumberedWaypoint] = []
|
||||
self.last_waypoint: Optional[FlightWaypoint] = None
|
||||
|
||||
def add_waypoint(self, waypoint_num: int, waypoint: FlightWaypoint) -> None:
|
||||
if waypoint.waypoint_type == FlightWaypointType.TARGET_POINT:
|
||||
@ -136,22 +140,59 @@ class FlightPlanBuilder:
|
||||
f"{first_waypoint_num}-{last_waypoint_num}",
|
||||
"Target points",
|
||||
"0",
|
||||
self._waypoint_distance(self.target_points[0].waypoint),
|
||||
self._ground_speed(self.target_points[0].waypoint),
|
||||
self._format_time(self.target_points[0].waypoint.tot),
|
||||
self._format_time(self.target_points[0].waypoint.departure_time),
|
||||
])
|
||||
self.last_waypoint = self.target_points[-1].waypoint
|
||||
|
||||
def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None:
|
||||
self.rows.append([
|
||||
str(waypoint.number),
|
||||
waypoint.waypoint.pretty_name,
|
||||
str(int(units.meters_to_feet(waypoint.waypoint.alt))),
|
||||
self._waypoint_distance(waypoint.waypoint),
|
||||
self._ground_speed(waypoint.waypoint),
|
||||
self._format_time(waypoint.waypoint.tot),
|
||||
self._format_time(waypoint.waypoint.departure_time),
|
||||
])
|
||||
self.last_waypoint = waypoint.waypoint
|
||||
|
||||
def _format_time(self, time: Optional[int]) -> str:
|
||||
if time is None:
|
||||
return ""
|
||||
local_time = self.start_time + datetime.timedelta(seconds=time)
|
||||
return local_time.strftime(f"%H:%M:%S LOCAL")
|
||||
return local_time.strftime(f"%H:%M:%S")
|
||||
|
||||
def _waypoint_distance(self, waypoint: FlightWaypoint) -> str:
|
||||
if self.last_waypoint is None:
|
||||
return "-"
|
||||
|
||||
distance = meter_to_nm(self.last_waypoint.position.distance_to_point(
|
||||
waypoint.position
|
||||
))
|
||||
return f"{distance} NM"
|
||||
|
||||
def _ground_speed(self, waypoint: FlightWaypoint) -> str:
|
||||
if self.last_waypoint is None:
|
||||
return "-"
|
||||
|
||||
if waypoint.tot is None:
|
||||
return "-"
|
||||
|
||||
if self.last_waypoint.departure_time is not None:
|
||||
last_time = self.last_waypoint.departure_time
|
||||
elif self.last_waypoint.tot is not None:
|
||||
last_time = self.last_waypoint.tot
|
||||
else:
|
||||
return "-"
|
||||
|
||||
distance = meter_to_nm(self.last_waypoint.position.distance_to_point(
|
||||
waypoint.position
|
||||
))
|
||||
duration = (waypoint.tot - last_time) / 3600
|
||||
return f"{int(distance / duration)} kt"
|
||||
|
||||
def build(self) -> List[List[str]]:
|
||||
return self.rows
|
||||
@ -186,8 +227,9 @@ class BriefingPage(KneeboardPage):
|
||||
flight_plan_builder = FlightPlanBuilder(self.start_time)
|
||||
for num, waypoint in enumerate(self.flight.waypoints):
|
||||
flight_plan_builder.add_waypoint(num, waypoint)
|
||||
writer.table(flight_plan_builder.build(),
|
||||
headers=["STPT", "Action", "Alt", "TOT"])
|
||||
writer.table(flight_plan_builder.build(), headers=[
|
||||
"#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure"
|
||||
])
|
||||
|
||||
writer.heading("Comm Ladder")
|
||||
comms = []
|
||||
|
||||
2
plugin/__init__.py
Normal file
2
plugin/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .luaplugin import LuaPlugin
|
||||
from .manager import LuaPluginManager
|
||||
199
plugin/luaplugin.py
Normal file
199
plugin/luaplugin.py
Normal file
@ -0,0 +1,199 @@
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
from PySide2.QtCore import QSize, Qt, QItemSelectionModel, QPoint
|
||||
from PySide2.QtWidgets import QLabel, QDialog, QGridLayout, QListView, QStackedLayout, QComboBox, QWidget, \
|
||||
QAbstractItemView, QPushButton, QGroupBox, QCheckBox, QVBoxLayout, QSpinBox
|
||||
import json
|
||||
|
||||
class LuaPluginWorkOrder():
|
||||
|
||||
def __init__(self, parent, filename:str, mnemonic:str, disable:bool):
|
||||
self.filename = filename
|
||||
self.mnemonic = mnemonic
|
||||
self.disable = disable
|
||||
self.parent = parent
|
||||
|
||||
def work(self, mnemonic:str, operation):
|
||||
if self.disable:
|
||||
operation.bypassPluginScript(self.parent.mnemonic, self.mnemonic)
|
||||
else:
|
||||
operation.injectPluginScript(self.parent.mnemonic, self.filename, self.mnemonic)
|
||||
|
||||
class LuaPluginSpecificOption():
|
||||
|
||||
def __init__(self, parent, mnemonic:str, nameInUI:str, defaultValue:bool):
|
||||
self.mnemonic = mnemonic
|
||||
self.nameInUI = nameInUI
|
||||
self.defaultValue = defaultValue
|
||||
self.parent = parent
|
||||
|
||||
class LuaPlugin():
|
||||
NAME_IN_SETTINGS_BASE:str = "plugins."
|
||||
|
||||
def __init__(self, jsonFilename:str):
|
||||
self.mnemonic:str = None
|
||||
self.skipUI:bool = False
|
||||
self.nameInUI:str = None
|
||||
self.nameInSettings:str = None
|
||||
self.defaultValue:bool = False
|
||||
self.specificOptions = []
|
||||
self.scriptsWorkOrders: List[LuaPluginWorkOrder] = None
|
||||
self.configurationWorkOrders: List[LuaPluginWorkOrder] = None
|
||||
self.initFromJson(jsonFilename)
|
||||
self.enabled = self.defaultValue
|
||||
self.settings = None
|
||||
|
||||
def initFromJson(self, jsonFilename:str):
|
||||
jsonFile:Path = Path(jsonFilename)
|
||||
if jsonFile.exists():
|
||||
jsonData = json.loads(jsonFile.read_text())
|
||||
self.mnemonic = jsonData.get("mnemonic")
|
||||
self.skipUI = jsonData.get("skipUI", False)
|
||||
self.nameInUI = jsonData.get("nameInUI")
|
||||
self.nameInSettings = LuaPlugin.NAME_IN_SETTINGS_BASE + self.mnemonic
|
||||
self.defaultValue = jsonData.get("defaultValue", False)
|
||||
self.specificOptions = []
|
||||
for jsonSpecificOption in jsonData.get("specificOptions"):
|
||||
mnemonic = jsonSpecificOption.get("mnemonic")
|
||||
nameInUI = jsonSpecificOption.get("nameInUI", mnemonic)
|
||||
defaultValue = jsonSpecificOption.get("defaultValue")
|
||||
self.specificOptions.append(LuaPluginSpecificOption(self, mnemonic, nameInUI, defaultValue))
|
||||
self.scriptsWorkOrders = []
|
||||
for jsonWorkOrder in jsonData.get("scriptsWorkOrders"):
|
||||
file = jsonWorkOrder.get("file")
|
||||
mnemonic = jsonWorkOrder.get("mnemonic")
|
||||
disable = jsonWorkOrder.get("disable", False)
|
||||
self.scriptsWorkOrders.append(LuaPluginWorkOrder(self, file, mnemonic, disable))
|
||||
self.configurationWorkOrders = []
|
||||
for jsonWorkOrder in jsonData.get("configurationWorkOrders"):
|
||||
file = jsonWorkOrder.get("file")
|
||||
mnemonic = jsonWorkOrder.get("mnemonic")
|
||||
disable = jsonWorkOrder.get("disable", False)
|
||||
self.configurationWorkOrders.append(LuaPluginWorkOrder(self, file, mnemonic, disable))
|
||||
|
||||
def setupUI(self, settingsWindow, row:int):
|
||||
# set the game settings
|
||||
self.setSettings(settingsWindow.game.settings)
|
||||
|
||||
if not self.skipUI:
|
||||
# create the plugin choice checkbox interface
|
||||
self.uiWidget: QCheckBox = QCheckBox()
|
||||
self.uiWidget.setChecked(self.isEnabled())
|
||||
self.uiWidget.toggled.connect(lambda: self.applySetting(settingsWindow))
|
||||
|
||||
settingsWindow.pluginsGroupLayout.addWidget(QLabel(self.nameInUI), row, 0)
|
||||
settingsWindow.pluginsGroupLayout.addWidget(self.uiWidget, row, 1, Qt.AlignRight)
|
||||
|
||||
# if needed, create the plugin options special page
|
||||
if settingsWindow.pluginsOptionsPageLayout and self.specificOptions != None:
|
||||
self.optionsGroup: QGroupBox = QGroupBox(self.nameInUI)
|
||||
optionsGroupLayout = QGridLayout();
|
||||
optionsGroupLayout.setAlignment(Qt.AlignTop)
|
||||
self.optionsGroup.setLayout(optionsGroupLayout)
|
||||
settingsWindow.pluginsOptionsPageLayout.addWidget(self.optionsGroup)
|
||||
|
||||
# browse each option in the specific options list
|
||||
row = 0
|
||||
for specificOption in self.specificOptions:
|
||||
nameInSettings = self.nameInSettings + "." + specificOption.mnemonic
|
||||
if not nameInSettings in self.settings.plugins:
|
||||
self.settings.plugins[nameInSettings] = specificOption.defaultValue
|
||||
|
||||
specificOption.uiWidget = QCheckBox()
|
||||
specificOption.uiWidget.setChecked(self.settings.plugins[nameInSettings])
|
||||
#specificOption.uiWidget.setEnabled(False)
|
||||
specificOption.uiWidget.toggled.connect(lambda: self.applySetting(settingsWindow))
|
||||
|
||||
optionsGroupLayout.addWidget(QLabel(specificOption.nameInUI), row, 0)
|
||||
optionsGroupLayout.addWidget(specificOption.uiWidget, row, 1, Qt.AlignRight)
|
||||
|
||||
row += 1
|
||||
|
||||
# disable or enable the UI in the plugins special page
|
||||
self.enableOptionsGroup()
|
||||
|
||||
def enableOptionsGroup(self):
|
||||
if self.optionsGroup:
|
||||
self.optionsGroup.setEnabled(self.isEnabled())
|
||||
|
||||
def setSettings(self, settings):
|
||||
self.settings = settings
|
||||
|
||||
# ensure the setting exist
|
||||
if not self.nameInSettings in self.settings.plugins:
|
||||
self.settings.plugins[self.nameInSettings] = self.defaultValue
|
||||
|
||||
# do the same for each option in the specific options list
|
||||
for specificOption in self.specificOptions:
|
||||
nameInSettings = self.nameInSettings + "." + specificOption.mnemonic
|
||||
if not nameInSettings in self.settings.plugins:
|
||||
self.settings.plugins[nameInSettings] = specificOption.defaultValue
|
||||
|
||||
def applySetting(self, settingsWindow):
|
||||
# apply the main setting
|
||||
self.settings.plugins[self.nameInSettings] = self.uiWidget.isChecked()
|
||||
self.enabled = self.settings.plugins[self.nameInSettings]
|
||||
|
||||
# do the same for each option in the specific options list
|
||||
for specificOption in self.specificOptions:
|
||||
nameInSettings = self.nameInSettings + "." + specificOption.mnemonic
|
||||
self.settings.plugins[nameInSettings] = specificOption.uiWidget.isChecked()
|
||||
|
||||
# disable or enable the UI in the plugins special page
|
||||
self.enableOptionsGroup()
|
||||
|
||||
def injectScripts(self, operation):
|
||||
# set the game settings
|
||||
self.setSettings(operation.game.settings)
|
||||
|
||||
# execute the work order
|
||||
if self.scriptsWorkOrders != None:
|
||||
for workOrder in self.scriptsWorkOrders:
|
||||
workOrder.work(self.mnemonic, operation)
|
||||
|
||||
# serves for subclasses
|
||||
return self.isEnabled()
|
||||
|
||||
def injectConfiguration(self, operation):
|
||||
# set the game settings
|
||||
self.setSettings(operation.game.settings)
|
||||
|
||||
# inject the plugin options
|
||||
if len(self.specificOptions) > 0:
|
||||
defineAllOptions = ""
|
||||
for specificOption in self.specificOptions:
|
||||
nameInSettings = self.nameInSettings + "." + specificOption.mnemonic
|
||||
value = "true" if self.settings.plugins[nameInSettings] else "false"
|
||||
defineAllOptions += f" dcsLiberation.plugins.{self.mnemonic}.{specificOption.mnemonic} = {value} \n"
|
||||
|
||||
|
||||
lua = f"-- {self.mnemonic} plugin configuration.\n"
|
||||
lua += "\n"
|
||||
lua += "if dcsLiberation then\n"
|
||||
lua += " if not dcsLiberation.plugins then \n"
|
||||
lua += " dcsLiberation.plugins = {}\n"
|
||||
lua += " end\n"
|
||||
lua += f" dcsLiberation.plugins.{self.mnemonic} = {{}}\n"
|
||||
lua += defineAllOptions
|
||||
lua += "end"
|
||||
|
||||
operation.injectLuaTrigger(lua, f"{self.mnemonic} plugin configuration")
|
||||
|
||||
# execute the work order
|
||||
if self.configurationWorkOrders != None:
|
||||
for workOrder in self.configurationWorkOrders:
|
||||
workOrder.work(self.mnemonic, operation)
|
||||
|
||||
# serves for subclasses
|
||||
return self.isEnabled()
|
||||
|
||||
def isEnabled(self) -> bool:
|
||||
if not self.settings:
|
||||
return False
|
||||
|
||||
self.setSettings(self.settings) # create the necessary settings keys if needed
|
||||
|
||||
return self.settings != None and self.settings.plugins[self.nameInSettings]
|
||||
|
||||
def hasUI(self) -> bool:
|
||||
return not self.skipUI
|
||||
43
plugin/manager.py
Normal file
43
plugin/manager.py
Normal file
@ -0,0 +1,43 @@
|
||||
from .luaplugin import LuaPlugin
|
||||
from typing import List
|
||||
import glob
|
||||
from pathlib import Path
|
||||
import json
|
||||
import logging
|
||||
|
||||
|
||||
class LuaPluginManager():
|
||||
PLUGINS_RESOURCE_PATH = Path("resources/plugins")
|
||||
PLUGINS_LIST_FILENAME = "plugins.json"
|
||||
PLUGINS_JSON_FILENAME = "plugin.json"
|
||||
|
||||
__plugins = None
|
||||
def __init__(self):
|
||||
if not LuaPluginManager.__plugins:
|
||||
LuaPluginManager.__plugins= []
|
||||
jsonFile:Path = Path(LuaPluginManager.PLUGINS_RESOURCE_PATH, LuaPluginManager.PLUGINS_LIST_FILENAME)
|
||||
if jsonFile.exists():
|
||||
logging.info(f"Reading plugins list from {jsonFile}")
|
||||
|
||||
jsonData = json.loads(jsonFile.read_text())
|
||||
for plugin in jsonData:
|
||||
jsonPluginFolder = Path(LuaPluginManager.PLUGINS_RESOURCE_PATH, plugin)
|
||||
jsonPluginFile = Path(jsonPluginFolder, LuaPluginManager.PLUGINS_JSON_FILENAME)
|
||||
if jsonPluginFile.exists():
|
||||
logging.info(f"Reading plugin {plugin} from {jsonPluginFile}")
|
||||
plugin = LuaPlugin(jsonPluginFile)
|
||||
LuaPluginManager.__plugins.append(plugin)
|
||||
else:
|
||||
logging.error(f"Missing configuration file {jsonPluginFile} for plugin {plugin}")
|
||||
else:
|
||||
logging.error(f"Missing plugins list file {jsonFile}")
|
||||
|
||||
def getPlugins(self):
|
||||
return LuaPluginManager.__plugins
|
||||
|
||||
def getPlugin(self, pluginName):
|
||||
for plugin in LuaPluginManager.__plugins:
|
||||
if plugin.mnemonic == pluginName:
|
||||
return plugin
|
||||
|
||||
return None
|
||||
@ -164,6 +164,7 @@ class PackageModel(QAbstractListModel):
|
||||
|
||||
def update_tot(self, tot: int) -> None:
|
||||
self.package.time_over_target = tot
|
||||
self.layoutChanged.emit()
|
||||
|
||||
@property
|
||||
def mission_target(self) -> MissionTarget:
|
||||
@ -234,7 +235,7 @@ class AtoModel(QAbstractListModel):
|
||||
"""Returns the package at the given index."""
|
||||
return self.ato.packages[index.row()]
|
||||
|
||||
def replace_from_game(self, game: Optional[Game]) -> None:
|
||||
def replace_from_game(self, game: Optional[Game], player: bool) -> None:
|
||||
"""Updates the ATO object to match the updated game object.
|
||||
|
||||
If the game is None (as is the case when no game has been loaded), an
|
||||
@ -244,7 +245,10 @@ class AtoModel(QAbstractListModel):
|
||||
self.game = game
|
||||
self.package_models.clear()
|
||||
if self.game is not None:
|
||||
self.ato = game.blue_ato
|
||||
if player:
|
||||
self.ato = game.blue_ato
|
||||
else:
|
||||
self.ato = game.red_ato
|
||||
else:
|
||||
self.ato = AirTaskingOrder()
|
||||
self.endResetModel()
|
||||
@ -268,8 +272,8 @@ class GameModel:
|
||||
"""
|
||||
def __init__(self) -> None:
|
||||
self.game: Optional[Game] = None
|
||||
# TODO: Add red ATO model, add cheat option to show red flight plan.
|
||||
self.ato_model = AtoModel(self.game, AirTaskingOrder())
|
||||
self.red_ato_model = AtoModel(self.game, AirTaskingOrder())
|
||||
|
||||
def set(self, game: Optional[Game]) -> None:
|
||||
"""Updates the managed Game object.
|
||||
@ -280,4 +284,5 @@ class GameModel:
|
||||
loaded.
|
||||
"""
|
||||
self.game = game
|
||||
self.ato_model.replace_from_game(self.game)
|
||||
self.ato_model.replace_from_game(self.game, player=True)
|
||||
self.red_ato_model.replace_from_game(self.game, player=False)
|
||||
|
||||
@ -112,6 +112,8 @@ def load_icons():
|
||||
ICONS["Generator"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/generator.png")
|
||||
ICONS["Missile"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/missile.png")
|
||||
ICONS["Cheat"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/cheat.png")
|
||||
ICONS["Plugins"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/plugins.png")
|
||||
ICONS["PluginsOptions"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/pluginsoptions.png")
|
||||
|
||||
ICONS["TaskCAS"] = QPixmap("./resources/ui/tasks/cas.png")
|
||||
ICONS["TaskCAP"] = QPixmap("./resources/ui/tasks/cap.png")
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
|
||||
from PySide2.QtWidgets import (
|
||||
QFrame,
|
||||
@ -11,6 +11,8 @@ from PySide2.QtWidgets import (
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game import Game
|
||||
from game.event import CAP, CAS, FrontlineAttackEvent
|
||||
from gen.ato import Package
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.widgets.QBudgetBox import QBudgetBox
|
||||
from qt_ui.widgets.QFactionsInfos import QFactionsInfos
|
||||
@ -95,7 +97,7 @@ class QTopPanel(QFrame):
|
||||
if game is None:
|
||||
return
|
||||
|
||||
self.turnCounter.setCurrentTurn(game.turn, game.current_day)
|
||||
self.turnCounter.setCurrentTurn(game.turn, game.conditions)
|
||||
self.budgetBox.setGame(game)
|
||||
self.factionsInfos.setGame(game)
|
||||
|
||||
@ -117,6 +119,24 @@ class QTopPanel(QFrame):
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
self.proceedButton.setEnabled(True)
|
||||
|
||||
def negative_start_packages(self) -> List[Package]:
|
||||
packages = []
|
||||
for package in self.game_model.ato_model.ato.packages:
|
||||
if not package.flights:
|
||||
continue
|
||||
estimator = TotEstimator(package)
|
||||
for flight in package.flights:
|
||||
if estimator.mission_start_time(flight) < 0:
|
||||
packages.append(package)
|
||||
break
|
||||
return packages
|
||||
|
||||
@staticmethod
|
||||
def fix_tots(packages: List[Package]) -> None:
|
||||
for package in packages:
|
||||
estimator = TotEstimator(package)
|
||||
package.time_over_target = estimator.earliest_tot()
|
||||
|
||||
def ato_has_clients(self) -> bool:
|
||||
for package in self.game.blue_ato.packages:
|
||||
for flight in package.flights:
|
||||
@ -142,12 +162,52 @@ class QTopPanel(QFrame):
|
||||
)
|
||||
return result == QMessageBox.Yes
|
||||
|
||||
def confirm_negative_start_time(self,
|
||||
negative_starts: List[Package]) -> bool:
|
||||
formatted = '<br />'.join(
|
||||
[f"{p.primary_task.name} {p.target.name}" for p in negative_starts]
|
||||
)
|
||||
mbox = QMessageBox(
|
||||
QMessageBox.Question,
|
||||
"Continue with past start times?",
|
||||
("Some flights in the following packages have start times set "
|
||||
"earlier than mission start time:<br />"
|
||||
"<br />"
|
||||
f"{formatted}<br />"
|
||||
"<br />"
|
||||
"Flight start times are estimated based on the package TOT, so it "
|
||||
"is possible that not all flights will be able to reach the "
|
||||
"target area at their assigned times.<br />"
|
||||
"<br />"
|
||||
"You can either continue with the mission as planned, with the "
|
||||
"misplanned flights potentially flying too fast and/or missing "
|
||||
"their rendezvous; automatically fix negative TOTs; or cancel "
|
||||
"mission start and fix the packages manually."),
|
||||
parent=self
|
||||
)
|
||||
auto = mbox.addButton("Fix TOTs automatically", QMessageBox.ActionRole)
|
||||
ignore = mbox.addButton("Continue without fixing",
|
||||
QMessageBox.DestructiveRole)
|
||||
cancel = mbox.addButton(QMessageBox.Cancel)
|
||||
mbox.setEscapeButton(cancel)
|
||||
mbox.exec_()
|
||||
clicked = mbox.clickedButton()
|
||||
if clicked == auto:
|
||||
self.fix_tots(negative_starts)
|
||||
return True
|
||||
elif clicked == ignore:
|
||||
return True
|
||||
return False
|
||||
|
||||
def launch_mission(self):
|
||||
"""Finishes planning and waits for mission completion."""
|
||||
if not self.ato_has_clients() and not self.confirm_no_client_launch():
|
||||
return
|
||||
|
||||
# TODO: Verify no negative start times.
|
||||
negative_starts = self.negative_start_packages()
|
||||
if negative_starts:
|
||||
if not self.confirm_negative_start_time(negative_starts):
|
||||
return
|
||||
|
||||
# TODO: Refactor this nonsense.
|
||||
game_event = None
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import datetime
|
||||
|
||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox
|
||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QVBoxLayout
|
||||
|
||||
from game.weather import Conditions, TimeOfDay
|
||||
import qt_ui.uiconstants as CONST
|
||||
|
||||
|
||||
@ -13,23 +14,37 @@ class QTurnCounter(QGroupBox):
|
||||
def __init__(self):
|
||||
super(QTurnCounter, self).__init__("Turn")
|
||||
|
||||
self.icons = [CONST.ICONS["Dawn"], CONST.ICONS["Day"], CONST.ICONS["Dusk"], CONST.ICONS["Night"]]
|
||||
|
||||
self.daytime_icon = QLabel()
|
||||
self.daytime_icon.setPixmap(self.icons[0])
|
||||
self.turn_info = QLabel()
|
||||
self.icons = {
|
||||
TimeOfDay.Dawn: CONST.ICONS["Dawn"],
|
||||
TimeOfDay.Day: CONST.ICONS["Day"],
|
||||
TimeOfDay.Dusk: CONST.ICONS["Dusk"],
|
||||
TimeOfDay.Night: CONST.ICONS["Night"],
|
||||
}
|
||||
|
||||
self.layout = QHBoxLayout()
|
||||
self.layout.addWidget(self.daytime_icon)
|
||||
self.layout.addWidget(self.turn_info)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def setCurrentTurn(self, turn: int, current_day: datetime):
|
||||
self.daytime_icon = QLabel()
|
||||
self.daytime_icon.setPixmap(self.icons[TimeOfDay.Dawn])
|
||||
self.layout.addWidget(self.daytime_icon)
|
||||
|
||||
self.time_column = QVBoxLayout()
|
||||
self.layout.addLayout(self.time_column)
|
||||
|
||||
self.date_display = QLabel()
|
||||
self.time_column.addWidget(self.date_display)
|
||||
|
||||
self.time_display = QLabel()
|
||||
self.time_column.addWidget(self.time_display)
|
||||
|
||||
def setCurrentTurn(self, turn: int, conditions: Conditions) -> None:
|
||||
"""Sets the turn information display.
|
||||
|
||||
:arg turn Current turn number.
|
||||
:arg conditions Current time and weather conditions.
|
||||
"""
|
||||
Set the money amount to display
|
||||
:arg turn Current turn number
|
||||
:arg current_day Current day
|
||||
"""
|
||||
self.daytime_icon.setPixmap(self.icons[turn % 4])
|
||||
self.turn_info.setText(current_day.strftime("%d %b %Y"))
|
||||
self.daytime_icon.setPixmap(self.icons[conditions.time_of_day])
|
||||
self.date_display.setText(conditions.start_time.strftime("%d %b %Y"))
|
||||
self.time_display.setText(
|
||||
conditions.start_time.strftime("%H:%M:%S Local"))
|
||||
self.setTitle("Turn " + str(turn + 1))
|
||||
|
||||
@ -337,6 +337,7 @@ class QPackageList(QListView):
|
||||
self.setItemDelegate(PackageDelegate())
|
||||
self.setIconSize(QSize(91, 24))
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectItems)
|
||||
self.model().rowsInserted.connect(self.on_new_packages)
|
||||
|
||||
@property
|
||||
def selected_item(self) -> Optional[Package]:
|
||||
@ -346,6 +347,14 @@ class QPackageList(QListView):
|
||||
return None
|
||||
return self.ato_model.package_at_index(index)
|
||||
|
||||
def on_new_packages(self, _parent: QModelIndex, first: int,
|
||||
_last: int) -> None:
|
||||
# Select the newly created pacakges. This should only ever happen due to
|
||||
# the player saving a new package, so selecting it helps them view/edit
|
||||
# it faster.
|
||||
self.selectionModel().setCurrentIndex(self.model().index(first, 0),
|
||||
QItemSelectionModel.Select)
|
||||
|
||||
|
||||
class QPackagePanel(QGroupBox):
|
||||
"""The package display portion of the ATO panel.
|
||||
@ -357,7 +366,7 @@ class QPackagePanel(QGroupBox):
|
||||
def __init__(self, model: AtoModel) -> None:
|
||||
super().__init__("Packages")
|
||||
self.ato_model = model
|
||||
self.ato_model.layoutChanged.connect(self.on_selection_changed)
|
||||
self.ato_model.layoutChanged.connect(self.on_current_changed)
|
||||
|
||||
self.vbox = QVBoxLayout()
|
||||
self.setLayout(self.vbox)
|
||||
@ -378,15 +387,15 @@ class QPackagePanel(QGroupBox):
|
||||
self.delete_button.clicked.connect(self.on_delete)
|
||||
self.button_row.addWidget(self.delete_button)
|
||||
|
||||
self.selection_changed.connect(self.on_selection_changed)
|
||||
self.on_selection_changed()
|
||||
self.current_changed.connect(self.on_current_changed)
|
||||
self.on_current_changed()
|
||||
|
||||
@property
|
||||
def selection_changed(self):
|
||||
def current_changed(self):
|
||||
"""Returns the signal emitted when the flight selection changes."""
|
||||
return self.package_list.selectionModel().selectionChanged
|
||||
return self.package_list.selectionModel().currentChanged
|
||||
|
||||
def on_selection_changed(self) -> None:
|
||||
def on_current_changed(self) -> None:
|
||||
"""Updates the status of the edit and delete buttons."""
|
||||
index = self.package_list.currentIndex()
|
||||
enabled = index.isValid()
|
||||
@ -436,8 +445,7 @@ class QAirTaskingOrderPanel(QSplitter):
|
||||
self.ato_model = game_model.ato_model
|
||||
|
||||
self.package_panel = QPackagePanel(self.ato_model)
|
||||
self.package_panel.selection_changed.connect(self.on_package_change)
|
||||
self.ato_model.rowsInserted.connect(self.on_package_change)
|
||||
self.package_panel.current_changed.connect(self.on_package_change)
|
||||
self.addWidget(self.package_panel)
|
||||
|
||||
self.flight_panel = QFlightPanel(game_model)
|
||||
|
||||
@ -19,7 +19,6 @@ class QFlightTypeComboBox(QComboBox):
|
||||
|
||||
COMMON_ENEMY_MISSIONS = [
|
||||
FlightType.ESCORT,
|
||||
FlightType.TARCAP,
|
||||
FlightType.SEAD,
|
||||
FlightType.DEAD,
|
||||
# TODO: FlightType.ELINT,
|
||||
@ -27,42 +26,46 @@ class QFlightTypeComboBox(QComboBox):
|
||||
# TODO: FlightType.RECON,
|
||||
]
|
||||
|
||||
FRIENDLY_AIRBASE_MISSIONS = [
|
||||
FlightType.CAP,
|
||||
# TODO: FlightType.INTERCEPTION
|
||||
# TODO: FlightType.LOGISTICS
|
||||
COMMON_FRIENDLY_MISSIONS = [
|
||||
FlightType.BARCAP,
|
||||
]
|
||||
|
||||
FRIENDLY_AIRBASE_MISSIONS = [
|
||||
# TODO: FlightType.INTERCEPTION
|
||||
# TODO: FlightType.LOGISTICS
|
||||
] + COMMON_FRIENDLY_MISSIONS
|
||||
|
||||
FRIENDLY_CARRIER_MISSIONS = [
|
||||
FlightType.BARCAP,
|
||||
# TODO: FlightType.INTERCEPTION
|
||||
# TODO: Buddy tanking for the A-4?
|
||||
# TODO: Rescue chopper?
|
||||
# TODO: Inter-ship logistics?
|
||||
]
|
||||
] + COMMON_FRIENDLY_MISSIONS
|
||||
|
||||
ENEMY_CARRIER_MISSIONS = [
|
||||
FlightType.ESCORT,
|
||||
FlightType.TARCAP,
|
||||
FlightType.BARCAP,
|
||||
# TODO: FlightType.ANTISHIP
|
||||
]
|
||||
|
||||
ENEMY_AIRBASE_MISSIONS = [
|
||||
FlightType.BARCAP,
|
||||
# TODO: FlightType.STRIKE
|
||||
] + COMMON_ENEMY_MISSIONS
|
||||
|
||||
FRIENDLY_GROUND_OBJECT_MISSIONS = [
|
||||
FlightType.CAP,
|
||||
# TODO: FlightType.LOGISTICS
|
||||
# TODO: FlightType.TROOP_TRANSPORT
|
||||
]
|
||||
] + COMMON_FRIENDLY_MISSIONS
|
||||
|
||||
ENEMY_GROUND_OBJECT_MISSIONS = [
|
||||
FlightType.BARCAP,
|
||||
FlightType.STRIKE,
|
||||
] + COMMON_ENEMY_MISSIONS
|
||||
|
||||
FRONT_LINE_MISSIONS = [
|
||||
FlightType.CAS,
|
||||
FlightType.TARCAP,
|
||||
# TODO: FlightType.TROOP_TRANSPORT
|
||||
# TODO: FlightType.EVAC
|
||||
] + COMMON_ENEMY_MISSIONS
|
||||
|
||||
@ -39,3 +39,9 @@ class QOriginAirfieldSelector(QComboBox):
|
||||
self.addItem(f"{origin.name} ({available} available)", origin)
|
||||
self.model().sort(0)
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def available(self) -> int:
|
||||
origin = self.currentData()
|
||||
inventory = self.global_inventory.for_control_point(origin)
|
||||
return inventory.available(self.aircraft)
|
||||
|
||||
@ -4,8 +4,16 @@ import datetime
|
||||
import logging
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from PySide2.QtCore import Qt, QPointF
|
||||
from PySide2.QtGui import QBrush, QColor, QPen, QPixmap, QWheelEvent, QPolygonF
|
||||
from PySide2.QtCore import QPointF, Qt
|
||||
from PySide2.QtGui import (
|
||||
QBrush,
|
||||
QColor,
|
||||
QFont,
|
||||
QPen,
|
||||
QPixmap,
|
||||
QPolygonF,
|
||||
QWheelEvent,
|
||||
)
|
||||
from PySide2.QtWidgets import (
|
||||
QFrame,
|
||||
QGraphicsItem,
|
||||
@ -21,6 +29,7 @@ from game import Game, db
|
||||
from game.data.aaa_db import AAA_UNITS
|
||||
from game.data.radar_db import UNITS_WITH_RADAR
|
||||
from game.utils import meter_to_feet
|
||||
from game.weather import TimeOfDay
|
||||
from gen import Conflict, PackageWaypointTiming
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
|
||||
@ -45,6 +54,9 @@ class QLiberationMap(QGraphicsView):
|
||||
self.game_model = game_model
|
||||
self.game: Optional[Game] = game_model.game
|
||||
|
||||
self.waypoint_info_font = QFont()
|
||||
self.waypoint_info_font.setPointSize(12)
|
||||
|
||||
self.flight_path_items: List[QGraphicsItem] = []
|
||||
# A tuple of (package index, flight index), or none.
|
||||
self.selected_flight: Optional[Tuple[int, int]] = None
|
||||
@ -55,25 +67,42 @@ class QLiberationMap(QGraphicsView):
|
||||
self.factor = 1
|
||||
self.factorized = 1
|
||||
self.init_scene()
|
||||
self.connectSignals()
|
||||
self.setGame(game_model.game)
|
||||
|
||||
GameUpdateSignal.get_instance().flight_paths_changed.connect(
|
||||
lambda: self.draw_flight_plans(self.scene())
|
||||
)
|
||||
|
||||
def update_package_selection(index: Optional[int]) -> None:
|
||||
self.selected_flight = index, 0
|
||||
def update_package_selection(index: int) -> None:
|
||||
# Optional[int] isn't a valid type for a Qt signal. None will be
|
||||
# converted to zero automatically. We use -1 to indicate no
|
||||
# selection.
|
||||
if index == -1:
|
||||
self.selected_flight = None
|
||||
else:
|
||||
self.selected_flight = index, 0
|
||||
self.draw_flight_plans(self.scene())
|
||||
|
||||
GameUpdateSignal.get_instance().package_selection_changed.connect(
|
||||
update_package_selection
|
||||
)
|
||||
|
||||
def update_flight_selection(index: Optional[int]) -> None:
|
||||
def update_flight_selection(index: int) -> None:
|
||||
if self.selected_flight is None:
|
||||
logging.error("Flight was selected with no package selected")
|
||||
if index != -1:
|
||||
# We don't know what order update_package_selection and
|
||||
# update_flight_selection will be called in when the last
|
||||
# package is removed. If no flight is selected, it's not a
|
||||
# problem to also have no package selected.
|
||||
logging.error(
|
||||
"Flight was selected with no package selected")
|
||||
return
|
||||
|
||||
# Optional[int] isn't a valid type for a Qt signal. None will be
|
||||
# converted to zero automatically. We use -1 to indicate no
|
||||
# selection.
|
||||
if index == -1:
|
||||
self.selected_flight = self.selected_flight[0], None
|
||||
self.selected_flight = self.selected_flight[0], index
|
||||
self.draw_flight_plans(self.scene())
|
||||
|
||||
@ -90,9 +119,6 @@ class QLiberationMap(QGraphicsView):
|
||||
self.setFrameShape(QFrame.NoFrame)
|
||||
self.setDragMode(QGraphicsView.ScrollHandDrag)
|
||||
|
||||
def connectSignals(self):
|
||||
GameUpdateSignal.get_instance().gameupdated.connect(self.setGame)
|
||||
|
||||
def setGame(self, game: Optional[Game]):
|
||||
self.game = game
|
||||
logging.debug("Reloading Map Canvas")
|
||||
@ -244,7 +270,7 @@ class QLiberationMap(QGraphicsView):
|
||||
text.setDefaultTextColor(Qt.white)
|
||||
text.setPos(pos[0] + CONST.CP_SIZE + 1, pos[1] - CONST.CP_SIZE / 2 + 1)
|
||||
|
||||
def draw_flight_plans(self, scene) -> None:
|
||||
def clear_flight_paths(self, scene: QGraphicsScene) -> None:
|
||||
for item in self.flight_path_items:
|
||||
try:
|
||||
scene.removeItem(item)
|
||||
@ -252,12 +278,20 @@ class QLiberationMap(QGraphicsView):
|
||||
# Something may have caused those items to already be removed.
|
||||
pass
|
||||
self.flight_path_items.clear()
|
||||
|
||||
def draw_flight_plans(self, scene: QGraphicsScene) -> None:
|
||||
self.clear_flight_paths(scene)
|
||||
if DisplayOptions.flight_paths.hide:
|
||||
return
|
||||
packages = list(self.game_model.ato_model.packages)
|
||||
if self.game.settings.show_red_ato:
|
||||
packages.extend(self.game_model.red_ato_model.packages)
|
||||
for p_idx, package_model in enumerate(packages):
|
||||
for f_idx, flight in enumerate(package_model.flights):
|
||||
selected = (p_idx, f_idx) == self.selected_flight
|
||||
if self.selected_flight is None:
|
||||
selected = False
|
||||
else:
|
||||
selected = (p_idx, f_idx) == self.selected_flight
|
||||
if DisplayOptions.flight_paths.only_selected and not selected:
|
||||
continue
|
||||
self.draw_flight_plan(scene, package_model.package, flight,
|
||||
@ -322,19 +356,19 @@ class QLiberationMap(QGraphicsView):
|
||||
pen = QPen(QColor("black"), 0.3)
|
||||
brush = QColor("white")
|
||||
|
||||
def draw_text(text: str, x: int, y: int) -> None:
|
||||
item = scene.addSimpleText(text)
|
||||
item.setBrush(brush)
|
||||
item.setPen(pen)
|
||||
item.moveBy(x, y)
|
||||
item.setZValue(2)
|
||||
self.flight_path_items.append(item)
|
||||
text = "\n".join([
|
||||
f"{number} {waypoint.name}",
|
||||
f"{altitude} ft {altitude_type}",
|
||||
tot,
|
||||
])
|
||||
|
||||
draw_text(f"{number} {waypoint.name}", position[0] + 8,
|
||||
position[1] - 15)
|
||||
draw_text(f"{altitude} ft {altitude_type}", position[0] + 8,
|
||||
position[1] - 5)
|
||||
draw_text(tot, position[0] + 8, position[1] + 5)
|
||||
item = scene.addSimpleText(text, self.waypoint_info_font)
|
||||
item.setFlag(QGraphicsItem.ItemIgnoresTransformations)
|
||||
item.setBrush(brush)
|
||||
item.setPen(pen)
|
||||
item.moveBy(position[0] + 8, position[1])
|
||||
item.setZValue(2)
|
||||
self.flight_path_items.append(item)
|
||||
|
||||
def draw_flight_path(self, scene: QGraphicsScene, pos0: Tuple[int, int],
|
||||
pos1: Tuple[int, int], player: bool,
|
||||
@ -486,9 +520,9 @@ class QLiberationMap(QGraphicsView):
|
||||
scene.addPixmap(bg)
|
||||
|
||||
# Apply graphical effects to simulate current daytime
|
||||
if self.game.current_turn_daytime == "day":
|
||||
if self.game.current_turn_time_of_day == TimeOfDay.Day:
|
||||
pass
|
||||
elif self.game.current_turn_daytime == "night":
|
||||
elif self.game.current_turn_time_of_day == TimeOfDay.Night:
|
||||
ov = QPixmap(bg.width(), bg.height())
|
||||
ov.fill(CONST.COLORS["night_overlay"])
|
||||
overlay = scene.addPixmap(ov)
|
||||
|
||||
@ -7,6 +7,7 @@ from PySide2.QtWidgets import QGraphicsItem
|
||||
import qt_ui.uiconstants as const
|
||||
from game import Game
|
||||
from game.data.building_data import FORTIFICATION_BUILDINGS
|
||||
from game.db import REWARDS
|
||||
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
|
||||
from theater import ControlPoint, TheaterGroundObject
|
||||
from .QMapObject import QMapObject
|
||||
@ -27,7 +28,14 @@ class QMapGroundObject(QMapObject):
|
||||
self.buildings = buildings if buildings is not None else []
|
||||
self.setFlag(QGraphicsItem.ItemIgnoresTransformations, False)
|
||||
self.ground_object_dialog: Optional[QGroundObjectMenu] = None
|
||||
self.setToolTip(self.tooltip)
|
||||
|
||||
@property
|
||||
def tooltip(self) -> str:
|
||||
lines = [
|
||||
f"[{self.ground_object.obj_name}]",
|
||||
f"${self.production_per_turn} per turn",
|
||||
]
|
||||
if self.ground_object.groups:
|
||||
units = {}
|
||||
for g in self.ground_object.groups:
|
||||
@ -36,16 +44,23 @@ class QMapGroundObject(QMapObject):
|
||||
units[u.type] = units[u.type]+1
|
||||
else:
|
||||
units[u.type] = 1
|
||||
tooltip = "[" + self.ground_object.obj_name + "]" + "\n"
|
||||
|
||||
for unit in units.keys():
|
||||
tooltip = tooltip + str(unit) + "x" + str(units[unit]) + "\n"
|
||||
self.setToolTip(tooltip[:-1])
|
||||
lines.append(f"{unit} x {units[unit]}")
|
||||
else:
|
||||
tooltip = "[" + self.ground_object.obj_name + "]" + "\n"
|
||||
for building in buildings:
|
||||
for building in self.buildings:
|
||||
if not building.is_dead:
|
||||
tooltip = tooltip + str(building.dcs_identifier) + "\n"
|
||||
self.setToolTip(tooltip[:-1])
|
||||
lines.append(f"{building.dcs_identifier}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@property
|
||||
def production_per_turn(self) -> int:
|
||||
production = 0
|
||||
for g in self.control_point.ground_objects:
|
||||
if g.category in REWARDS.keys():
|
||||
production += REWARDS[g.category]
|
||||
return production
|
||||
|
||||
def paint(self, painter, option, widget=None) -> None:
|
||||
player_icons = "_blue"
|
||||
|
||||
@ -24,8 +24,8 @@ class GameUpdateSignal(QObject):
|
||||
debriefingReceived = Signal(DebriefingSignal)
|
||||
|
||||
flight_paths_changed = Signal()
|
||||
package_selection_changed = Signal(int) # Optional[int]
|
||||
flight_selection_changed = Signal(int) # Optional[int]
|
||||
package_selection_changed = Signal(int) # -1 indicates no selection.
|
||||
flight_selection_changed = Signal(int) # -1 indicates no selection.
|
||||
|
||||
def __init__(self):
|
||||
super(GameUpdateSignal, self).__init__()
|
||||
@ -33,11 +33,11 @@ class GameUpdateSignal(QObject):
|
||||
|
||||
def select_package(self, index: Optional[int]) -> None:
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.package_selection_changed.emit(index)
|
||||
self.package_selection_changed.emit(-1 if index is None else index)
|
||||
|
||||
def select_flight(self, index: Optional[int]) -> None:
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.flight_selection_changed.emit(index)
|
||||
self.flight_selection_changed.emit(-1 if index is None else index)
|
||||
|
||||
def redraw_flight_paths(self) -> None:
|
||||
# noinspection PyUnresolvedReferences
|
||||
|
||||
@ -43,6 +43,7 @@ class QLiberationWindow(QMainWindow):
|
||||
Dialog.set_game(self.game_model)
|
||||
self.ato_panel = None
|
||||
self.info_panel = None
|
||||
self.liberation_map = None
|
||||
self.setGame(persistency.restore_game())
|
||||
|
||||
self.setGeometry(300, 100, 270, 100)
|
||||
@ -224,9 +225,11 @@ class QLiberationWindow(QMainWindow):
|
||||
if game is not None:
|
||||
game.on_load()
|
||||
self.game = game
|
||||
if self.info_panel:
|
||||
if self.info_panel is not None:
|
||||
self.info_panel.setGame(game)
|
||||
self.game_model.set(self.game)
|
||||
if self.liberation_map is not None:
|
||||
self.liberation_map.setGame(game)
|
||||
|
||||
def showAboutDialog(self):
|
||||
text = "<h3>DCS Liberation " + CONST.VERSION_STRING + "</h3>" + \
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
from typing import Optional
|
||||
from typing import Optional, Set
|
||||
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import (
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
QScrollArea,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QScrollArea,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
from game.event.event import UnitsDeliveryEvent
|
||||
from qt_ui.models import GameModel
|
||||
@ -48,26 +49,27 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
|
||||
def init_ui(self):
|
||||
main_layout = QVBoxLayout()
|
||||
|
||||
units = {
|
||||
CAP: db.find_unittype(CAP, self.game_model.game.player_name),
|
||||
CAS: db.find_unittype(CAS, self.game_model.game.player_name),
|
||||
}
|
||||
tasks = [CAP, CAS]
|
||||
|
||||
scroll_content = QWidget()
|
||||
task_box_layout = QGridLayout()
|
||||
row = 0
|
||||
|
||||
for task_type in units.keys():
|
||||
units_column = list(set(units[task_type]))
|
||||
if len(units_column) == 0:
|
||||
unit_types: Set[UnitType] = set()
|
||||
for task in tasks:
|
||||
units = db.find_unittype(task, self.game_model.game.player_name)
|
||||
if not units:
|
||||
continue
|
||||
units_column.sort(key=lambda x: db.PRICES[x])
|
||||
for unit_type in units_column:
|
||||
if self.cp.is_carrier and not unit_type in db.CARRIER_CAPABLE:
|
||||
for unit in units:
|
||||
if self.cp.is_carrier and unit not in db.CARRIER_CAPABLE:
|
||||
continue
|
||||
if self.cp.is_lha and not unit_type in db.LHA_CAPABLE:
|
||||
if self.cp.is_lha and unit not in db.LHA_CAPABLE:
|
||||
continue
|
||||
row = self.add_purchase_row(unit_type, task_box_layout, row)
|
||||
unit_types.add(unit)
|
||||
|
||||
sorted_units = sorted(unit_types, key=lambda u: db.unit_type_name_2(u))
|
||||
for unit_type in sorted_units:
|
||||
row = self.add_purchase_row(unit_type, task_box_layout, row)
|
||||
stretch = QVBoxLayout()
|
||||
stretch.addStretch()
|
||||
task_box_layout.addLayout(stretch, row, 0)
|
||||
|
||||
@ -16,6 +16,7 @@ from game.game import Game
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from gen.flights.flightplan import FlightPlanBuilder
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from qt_ui.models import AtoModel, PackageModel
|
||||
from qt_ui.uiconstants import EVENT_ICONS
|
||||
from qt_ui.widgets.ato import QFlightList
|
||||
@ -34,12 +35,6 @@ class QPackageDialog(QDialog):
|
||||
#: Emitted when a change is made to the package.
|
||||
package_changed = Signal()
|
||||
|
||||
#: Emitted when a flight is added to the package.
|
||||
flight_added = Signal(Flight)
|
||||
|
||||
#: Emitted when a flight is removed from the package.
|
||||
flight_removed = Signal(Flight)
|
||||
|
||||
def __init__(self, game: Game, model: PackageModel) -> None:
|
||||
super().__init__()
|
||||
self.game = game
|
||||
@ -77,20 +72,20 @@ class QPackageDialog(QDialog):
|
||||
self.tot_label = QLabel("Time Over Target:")
|
||||
self.tot_column.addWidget(self.tot_label)
|
||||
|
||||
if self.package_model.package.time_over_target is None:
|
||||
time = None
|
||||
else:
|
||||
delay = self.package_model.package.time_over_target
|
||||
hours = delay // 3600
|
||||
minutes = delay // 60 % 60
|
||||
seconds = delay % 60
|
||||
time = QTime(hours, minutes, seconds)
|
||||
|
||||
self.tot_spinner = QTimeEdit(time)
|
||||
self.tot_spinner = QTimeEdit(self.tot_qtime())
|
||||
self.tot_spinner.setMinimumTime(QTime(0, 0))
|
||||
self.tot_spinner.setDisplayFormat("T+hh:mm:ss")
|
||||
self.tot_spinner.timeChanged.connect(self.save_tot)
|
||||
self.tot_column.addWidget(self.tot_spinner)
|
||||
|
||||
self.reset_tot_button = QPushButton("Reset TOT")
|
||||
self.reset_tot_button.setToolTip(
|
||||
"Sets the package TOT to the earliest time that all flights can "
|
||||
"arrive at the target."
|
||||
)
|
||||
self.reset_tot_button.clicked.connect(self.reset_tot)
|
||||
self.tot_column.addWidget(self.reset_tot_button)
|
||||
|
||||
self.package_view = QFlightList(self.package_model)
|
||||
self.package_view.selectionModel().selectionChanged.connect(
|
||||
self.on_selection_changed
|
||||
@ -107,24 +102,47 @@ class QPackageDialog(QDialog):
|
||||
self.delete_flight_button = QPushButton("Delete Selected")
|
||||
self.delete_flight_button.setProperty("style", "btn-danger")
|
||||
self.delete_flight_button.clicked.connect(self.on_delete_flight)
|
||||
self.delete_flight_button.setEnabled(False)
|
||||
self.delete_flight_button.setEnabled(model.rowCount() > 0)
|
||||
self.button_layout.addWidget(self.delete_flight_button)
|
||||
|
||||
self.button_layout.addStretch()
|
||||
|
||||
self.setLayout(self.layout)
|
||||
|
||||
self.accepted.connect(self.on_save)
|
||||
self.finished.connect(self.on_close)
|
||||
self.rejected.connect(self.on_cancel)
|
||||
|
||||
def tot_qtime(self) -> QTime:
|
||||
delay = self.package_model.package.time_over_target
|
||||
hours = delay // 3600
|
||||
minutes = delay // 60 % 60
|
||||
seconds = delay % 60
|
||||
return QTime(hours, minutes, seconds)
|
||||
|
||||
def on_cancel(self) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def on_close(_result) -> None:
|
||||
GameUpdateSignal.get_instance().redraw_flight_paths()
|
||||
|
||||
def on_save(self) -> None:
|
||||
self.save_tot()
|
||||
|
||||
def save_tot(self) -> None:
|
||||
time = self.tot_spinner.time()
|
||||
seconds = time.hour() * 3600 + time.minute() * 60 + time.second()
|
||||
self.package_model.update_tot(seconds)
|
||||
|
||||
def reset_tot(self) -> None:
|
||||
if not list(self.package_model.flights):
|
||||
self.package_model.update_tot(0)
|
||||
else:
|
||||
self.package_model.update_tot(
|
||||
TotEstimator(self.package_model.package).earliest_tot())
|
||||
self.tot_spinner.setTime(self.tot_qtime())
|
||||
|
||||
def on_selection_changed(self, selected: QItemSelection,
|
||||
_deselected: QItemSelection) -> None:
|
||||
"""Updates the state of the delete button."""
|
||||
@ -139,14 +157,13 @@ class QPackageDialog(QDialog):
|
||||
|
||||
def add_flight(self, flight: Flight) -> None:
|
||||
"""Adds the new flight to the package."""
|
||||
self.game.aircraft_inventory.claim_for_flight(flight)
|
||||
self.package_model.add_flight(flight)
|
||||
planner = FlightPlanBuilder(self.game, self.package_model.package,
|
||||
is_player=True)
|
||||
planner.populate_flight_plan(flight)
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.package_changed.emit()
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.flight_added.emit(flight)
|
||||
|
||||
def on_delete_flight(self) -> None:
|
||||
"""Removes the selected flight from the package."""
|
||||
@ -154,11 +171,10 @@ class QPackageDialog(QDialog):
|
||||
if flight is None:
|
||||
logging.error(f"Cannot delete flight when no flight is selected.")
|
||||
return
|
||||
self.game.aircraft_inventory.return_from_flight(flight)
|
||||
self.package_model.delete_flight(flight)
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.package_changed.emit()
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.flight_removed.emit(flight)
|
||||
|
||||
|
||||
class QNewPackageDialog(QPackageDialog):
|
||||
@ -174,22 +190,22 @@ class QNewPackageDialog(QPackageDialog):
|
||||
|
||||
self.save_button = QPushButton("Save")
|
||||
self.save_button.setProperty("style", "start-button")
|
||||
self.save_button.clicked.connect(self.on_save)
|
||||
self.save_button.clicked.connect(self.accept)
|
||||
self.button_layout.addWidget(self.save_button)
|
||||
|
||||
self.delete_flight_button.clicked.connect(self.on_delete_flight)
|
||||
|
||||
def on_save(self) -> None:
|
||||
"""Saves the created package.
|
||||
|
||||
Empty packages may be created. They can be modified later, and will have
|
||||
no effect if empty when the mission is generated.
|
||||
"""
|
||||
self.save_tot()
|
||||
super().on_save()
|
||||
self.ato_model.add_package(self.package_model.package)
|
||||
|
||||
def on_cancel(self) -> None:
|
||||
super().on_cancel()
|
||||
for flight in self.package_model.package.flights:
|
||||
self.game.aircraft_inventory.claim_for_flight(flight)
|
||||
self.close()
|
||||
self.game.aircraft_inventory.return_from_flight(flight)
|
||||
|
||||
|
||||
class QEditPackageDialog(QPackageDialog):
|
||||
@ -210,30 +226,9 @@ class QEditPackageDialog(QPackageDialog):
|
||||
|
||||
self.done_button = QPushButton("Done")
|
||||
self.done_button.setProperty("style", "start-button")
|
||||
self.done_button.clicked.connect(self.on_done)
|
||||
self.done_button.clicked.connect(self.accept)
|
||||
self.button_layout.addWidget(self.done_button)
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.flight_added.connect(self.on_flight_added)
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.flight_removed.connect(self.on_flight_removed)
|
||||
|
||||
# TODO: Make the new package dialog do this too, return on cancel.
|
||||
# Not claiming the aircraft when they are added to the planner means that
|
||||
# inventory counts are not updated until after the new package is updated,
|
||||
# so you can add an infinite number of aircraft to a new package in the UI,
|
||||
# which will crash when the flight package is saved.
|
||||
def on_flight_added(self, flight: Flight) -> None:
|
||||
self.game.aircraft_inventory.claim_for_flight(flight)
|
||||
|
||||
def on_flight_removed(self, flight: Flight) -> None:
|
||||
self.game.aircraft_inventory.return_from_flight(flight)
|
||||
|
||||
def on_done(self) -> None:
|
||||
"""Closes the window."""
|
||||
self.save_tot()
|
||||
self.close()
|
||||
|
||||
def on_delete(self) -> None:
|
||||
"""Removes the viewed package from the ATO."""
|
||||
# The ATO model returns inventory for us when deleting a package.
|
||||
|
||||
@ -53,10 +53,23 @@ class QFlightCreator(QDialog):
|
||||
[cp for cp in game.theater.controlpoints if cp.captured],
|
||||
self.aircraft_selector.currentData()
|
||||
)
|
||||
self.aircraft_selector.currentIndexChanged.connect(self.update_max_size)
|
||||
layout.addLayout(QLabeledWidget("Airfield:", self.airfield_selector))
|
||||
|
||||
self.flight_size_spinner = QFlightSizeSpinner()
|
||||
layout.addLayout(QLabeledWidget("Count:", self.flight_size_spinner))
|
||||
self.update_max_size()
|
||||
layout.addLayout(QLabeledWidget("Size:", self.flight_size_spinner))
|
||||
|
||||
self.client_slots_spinner = QFlightSizeSpinner(
|
||||
min_size=0,
|
||||
max_size=self.flight_size_spinner.value(),
|
||||
default_size=0
|
||||
)
|
||||
self.flight_size_spinner.valueChanged.connect(
|
||||
lambda v: self.client_slots_spinner.setMaximum(v)
|
||||
)
|
||||
layout.addLayout(
|
||||
QLabeledWidget("Client Slots:", self.client_slots_spinner))
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
@ -96,6 +109,7 @@ class QFlightCreator(QDialog):
|
||||
start_type = "Warm"
|
||||
flight = Flight(aircraft, size, origin, task, start_type)
|
||||
flight.scheduled_in = self.package.delay
|
||||
flight.client_count = self.client_slots_spinner.value()
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.created.emit(flight)
|
||||
@ -104,3 +118,8 @@ class QFlightCreator(QDialog):
|
||||
def on_aircraft_changed(self, index: int) -> None:
|
||||
new_aircraft = self.aircraft_selector.itemData(index)
|
||||
self.airfield_selector.change_aircraft(new_aircraft)
|
||||
|
||||
def update_max_size(self) -> None:
|
||||
self.flight_size_spinner.setMaximum(
|
||||
min(self.airfield_selector.available, 4)
|
||||
)
|
||||
|
||||
@ -19,11 +19,15 @@ class QFlightPlanner(QTabWidget):
|
||||
def __init__(self, package: Package, flight: Flight, game: Game):
|
||||
super().__init__()
|
||||
|
||||
self.general_settings_tab = QGeneralFlightSettingsTab(game, flight)
|
||||
self.general_settings_tab = QGeneralFlightSettingsTab(
|
||||
game, package, flight
|
||||
)
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.general_settings_tab.on_flight_settings_changed.connect(
|
||||
lambda: self.on_planned_flight_changed.emit())
|
||||
self.payload_tab = QFlightPayloadTab(flight, game)
|
||||
self.waypoint_tab = QFlightWaypointTab(game, package, flight)
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.waypoint_tab.on_flight_changed.connect(
|
||||
lambda: self.on_planned_flight_changed.emit())
|
||||
self.addTab(self.general_settings_tab, "General Flight settings")
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
import datetime
|
||||
|
||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QVBoxLayout
|
||||
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
|
||||
|
||||
# TODO: Remove?
|
||||
class QFlightDepartureDisplay(QGroupBox):
|
||||
|
||||
def __init__(self, package: Package, flight: Flight):
|
||||
super().__init__("Departure")
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
departure_row = QHBoxLayout()
|
||||
layout.addLayout(departure_row)
|
||||
|
||||
estimator = TotEstimator(package)
|
||||
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
|
||||
|
||||
departure_row.addWidget(QLabel(
|
||||
f"Departing from <b>{flight.from_cp.name}</b>"
|
||||
))
|
||||
departure_row.addWidget(QLabel(f"At T+{delay}"))
|
||||
|
||||
layout.addWidget(QLabel("Determined based on the package TOT. Edit the "
|
||||
"package to adjust the TOT."))
|
||||
|
||||
self.setLayout(layout)
|
||||
@ -1,31 +0,0 @@
|
||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox
|
||||
|
||||
|
||||
# TODO: Remove?
|
||||
class QFlightDepartureEditor(QGroupBox):
|
||||
|
||||
def __init__(self, flight):
|
||||
super(QFlightDepartureEditor, self).__init__("Departure")
|
||||
self.flight = flight
|
||||
|
||||
layout = QHBoxLayout()
|
||||
self.depart_from = QLabel("Departing from <b>" + self.flight.from_cp.name + "</b>")
|
||||
self.depart_at_t = QLabel("At T +")
|
||||
self.minutes = QLabel(" minutes")
|
||||
|
||||
self.departure_delta = QSpinBox(self)
|
||||
self.departure_delta.setMinimum(0)
|
||||
self.departure_delta.setMaximum(120)
|
||||
self.departure_delta.setValue(self.flight.scheduled_in // 60)
|
||||
self.departure_delta.valueChanged.connect(self.change_scheduled)
|
||||
|
||||
layout.addWidget(self.depart_from)
|
||||
layout.addWidget(self.depart_at_t)
|
||||
layout.addWidget(self.departure_delta)
|
||||
layout.addWidget(self.minutes)
|
||||
self.setLayout(layout)
|
||||
|
||||
self.changed = self.departure_delta.valueChanged
|
||||
|
||||
def change_scheduled(self):
|
||||
self.flight.scheduled_in = int(self.departure_delta.value() * 60)
|
||||
@ -2,26 +2,29 @@ from PySide2.QtCore import Signal
|
||||
from PySide2.QtWidgets import QFrame, QGridLayout, QVBoxLayout
|
||||
|
||||
from game import Game
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from qt_ui.windows.mission.flight.settings.QFlightDepartureEditor import QFlightDepartureEditor
|
||||
from qt_ui.windows.mission.flight.settings.QFlightSlotEditor import QFlightSlotEditor
|
||||
from qt_ui.windows.mission.flight.settings.QFlightStartType import QFlightStartType
|
||||
from qt_ui.windows.mission.flight.settings.QFlightTypeTaskInfo import QFlightTypeTaskInfo
|
||||
from qt_ui.windows.mission.flight.settings.QFlightDepartureDisplay import \
|
||||
QFlightDepartureDisplay
|
||||
from qt_ui.windows.mission.flight.settings.QFlightSlotEditor import \
|
||||
QFlightSlotEditor
|
||||
from qt_ui.windows.mission.flight.settings.QFlightStartType import \
|
||||
QFlightStartType
|
||||
from qt_ui.windows.mission.flight.settings.QFlightTypeTaskInfo import \
|
||||
QFlightTypeTaskInfo
|
||||
|
||||
|
||||
class QGeneralFlightSettingsTab(QFrame):
|
||||
on_flight_settings_changed = Signal()
|
||||
|
||||
def __init__(self, game: Game, flight: Flight):
|
||||
super(QGeneralFlightSettingsTab, self).__init__()
|
||||
self.flight = flight
|
||||
self.game = game
|
||||
def __init__(self, game: Game, package: Package, flight: Flight):
|
||||
super().__init__()
|
||||
|
||||
layout = QGridLayout()
|
||||
flight_info = QFlightTypeTaskInfo(self.flight)
|
||||
flight_departure = QFlightDepartureEditor(self.flight)
|
||||
flight_slots = QFlightSlotEditor(self.flight, self.game)
|
||||
flight_start_type = QFlightStartType(self.flight)
|
||||
flight_info = QFlightTypeTaskInfo(flight)
|
||||
flight_departure = QFlightDepartureDisplay(package, flight)
|
||||
flight_slots = QFlightSlotEditor(flight, game)
|
||||
flight_start_type = QFlightStartType(flight)
|
||||
layout.addWidget(flight_info, 0, 0)
|
||||
layout.addWidget(flight_departure, 1, 0)
|
||||
layout.addWidget(flight_slots, 2, 0)
|
||||
@ -31,8 +34,6 @@ class QGeneralFlightSettingsTab(QFrame):
|
||||
layout.addLayout(vstretch, 3, 0)
|
||||
self.setLayout(layout)
|
||||
|
||||
flight_start_type.setEnabled(self.flight.client_count > 0)
|
||||
flight_start_type.setEnabled(flight.client_count > 0)
|
||||
flight_slots.changed.connect(
|
||||
lambda: flight_start_type.setEnabled(self.flight.client_count > 0))
|
||||
flight_departure.changed.connect(
|
||||
lambda: self.on_flight_settings_changed.emit())
|
||||
lambda: flight_start_type.setEnabled(flight.client_count > 0))
|
||||
|
||||
@ -54,6 +54,7 @@ class QFlightWaypointTab(QFrame):
|
||||
rlayout.addWidget(QLabel("<strong>Generator :</strong>"))
|
||||
rlayout.addWidget(QLabel("<small>AI compatible</small>"))
|
||||
|
||||
# TODO: Filter by objective type.
|
||||
self.recreate_buttons.clear()
|
||||
recreate_types = [
|
||||
FlightType.CAS,
|
||||
@ -137,13 +138,16 @@ class QFlightWaypointTab(QFrame):
|
||||
QMessageBox.Yes
|
||||
)
|
||||
if result == QMessageBox.Yes:
|
||||
# TODO: These should all be just CAP.
|
||||
# TODO: Should be buttons for both BARCAP and TARCAP.
|
||||
# BARCAP and TARCAP behave differently. TARCAP arrives a few minutes
|
||||
# ahead of the rest of the package and stays until the package
|
||||
# departs, whereas BARCAP usually isn't part of a strike package and
|
||||
# has a fixed mission time.
|
||||
if task == FlightType.CAP:
|
||||
if isinstance(self.package.target, FrontLine):
|
||||
task = FlightType.TARCAP
|
||||
elif isinstance(self.package.target, ControlPoint):
|
||||
if self.package.target.is_fleet:
|
||||
task = FlightType.BARCAP
|
||||
task = FlightType.BARCAP
|
||||
self.flight.flight_type = task
|
||||
self.planner.populate_flight_plan(self.flight)
|
||||
self.flight_waypoint_list.update_list()
|
||||
|
||||
@ -1,16 +1,48 @@
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
from PySide2.QtCore import QSize, Qt, QItemSelectionModel, QPoint
|
||||
from PySide2.QtGui import QStandardItemModel, QStandardItem
|
||||
from PySide2.QtWidgets import QLabel, QDialog, QGridLayout, QListView, QStackedLayout, QComboBox, QWidget, \
|
||||
QAbstractItemView, QPushButton, QGroupBox, QCheckBox, QVBoxLayout, QSpinBox
|
||||
from PySide2.QtWidgets import (
|
||||
QLabel,
|
||||
QDialog,
|
||||
QGridLayout,
|
||||
QListView,
|
||||
QStackedLayout,
|
||||
QComboBox,
|
||||
QWidget,
|
||||
QAbstractItemView,
|
||||
QPushButton,
|
||||
QGroupBox,
|
||||
QCheckBox,
|
||||
QVBoxLayout,
|
||||
QSpinBox,
|
||||
)
|
||||
from dcs.forcedoptions import ForcedOptions
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game.game import Game
|
||||
from game.infos.information import Information
|
||||
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.finances.QFinancesMenu import QHorizontalSeparationLine
|
||||
from plugin import LuaPluginManager
|
||||
|
||||
class CheatSettingsBox(QGroupBox):
|
||||
def __init__(self, game: Game, apply_settings: Callable[[], None]) -> None:
|
||||
super().__init__("Cheat Settings")
|
||||
self.main_layout = QVBoxLayout()
|
||||
self.setLayout(self.main_layout)
|
||||
|
||||
self.red_ato_checkbox = QCheckBox()
|
||||
self.red_ato_checkbox.setChecked(game.settings.show_red_ato)
|
||||
self.red_ato_checkbox.toggled.connect(apply_settings)
|
||||
self.red_ato = QLabeledWidget("Show Red ATO:", self.red_ato_checkbox)
|
||||
self.main_layout.addLayout(self.red_ato)
|
||||
|
||||
@property
|
||||
def show_red_ato(self) -> bool:
|
||||
return self.red_ato_checkbox.isChecked()
|
||||
|
||||
|
||||
class QSettingsWindow(QDialog):
|
||||
@ -19,6 +51,8 @@ class QSettingsWindow(QDialog):
|
||||
super(QSettingsWindow, self).__init__()
|
||||
|
||||
self.game = game
|
||||
self.pluginsPage = None
|
||||
self.pluginsOptionsPage = None
|
||||
|
||||
self.setModal(True)
|
||||
self.setWindowTitle("Settings")
|
||||
@ -37,38 +71,53 @@ class QSettingsWindow(QDialog):
|
||||
|
||||
self.categoryModel = QStandardItemModel(self.categoryList)
|
||||
|
||||
self.categoryList.setIconSize(QSize(32, 32))
|
||||
|
||||
self.initDifficultyLayout()
|
||||
difficulty = QStandardItem("Difficulty")
|
||||
difficulty.setIcon(CONST.ICONS["Missile"])
|
||||
difficulty.setEditable(False)
|
||||
difficulty.setSelectable(True)
|
||||
self.categoryModel.appendRow(difficulty)
|
||||
self.right_layout.addWidget(self.difficultyPage)
|
||||
|
||||
self.initGeneratorLayout()
|
||||
generator = QStandardItem("Mission Generator")
|
||||
generator.setIcon(CONST.ICONS["Generator"])
|
||||
generator.setEditable(False)
|
||||
generator.setSelectable(True)
|
||||
self.categoryModel.appendRow(generator)
|
||||
self.right_layout.addWidget(self.generatorPage)
|
||||
|
||||
self.initCheatLayout()
|
||||
cheat = QStandardItem("Cheat Menu")
|
||||
cheat.setIcon(CONST.ICONS["Cheat"])
|
||||
cheat.setEditable(False)
|
||||
cheat.setSelectable(True)
|
||||
|
||||
self.categoryList.setIconSize(QSize(32, 32))
|
||||
self.categoryModel.appendRow(difficulty)
|
||||
self.categoryModel.appendRow(generator)
|
||||
self.categoryModel.appendRow(cheat)
|
||||
self.right_layout.addWidget(self.cheatPage)
|
||||
|
||||
self.initPluginsLayout()
|
||||
if self.pluginsPage:
|
||||
plugins = QStandardItem("LUA Plugins")
|
||||
plugins.setIcon(CONST.ICONS["Plugins"])
|
||||
plugins.setEditable(False)
|
||||
plugins.setSelectable(True)
|
||||
self.categoryModel.appendRow(plugins)
|
||||
self.right_layout.addWidget(self.pluginsPage)
|
||||
if self.pluginsOptionsPage:
|
||||
pluginsOptions = QStandardItem("LUA Plugins Options")
|
||||
pluginsOptions.setIcon(CONST.ICONS["PluginsOptions"])
|
||||
pluginsOptions.setEditable(False)
|
||||
pluginsOptions.setSelectable(True)
|
||||
self.categoryModel.appendRow(pluginsOptions)
|
||||
self.right_layout.addWidget(self.pluginsOptionsPage)
|
||||
|
||||
self.categoryList.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.categoryList.setModel(self.categoryModel)
|
||||
self.categoryList.selectionModel().setCurrentIndex(self.categoryList.indexAt(QPoint(1,1)), QItemSelectionModel.Select)
|
||||
self.categoryList.selectionModel().selectionChanged.connect(self.onSelectionChanged)
|
||||
|
||||
self.initDifficultyLayout()
|
||||
self.initGeneratorLayout()
|
||||
self.initCheatLayout()
|
||||
|
||||
self.right_layout.addWidget(self.difficultyPage)
|
||||
self.right_layout.addWidget(self.generatorPage)
|
||||
self.right_layout.addWidget(self.cheatPage)
|
||||
|
||||
self.layout.addWidget(self.categoryList, 0, 0, 1, 1)
|
||||
self.layout.addLayout(self.right_layout, 0, 1, 5, 1)
|
||||
@ -168,28 +217,10 @@ class QSettingsWindow(QDialog):
|
||||
self.generate_marks.setChecked(self.game.settings.generate_marks)
|
||||
self.generate_marks.toggled.connect(self.applySettings)
|
||||
|
||||
|
||||
if not hasattr(self.game.settings, "include_jtac_if_available"):
|
||||
self.game.settings.include_jtac_if_available = True
|
||||
if not hasattr(self.game.settings, "jtac_smoke_on"):
|
||||
self.game.settings.jtac_smoke_on= True
|
||||
|
||||
self.include_jtac_if_available = QCheckBox()
|
||||
self.include_jtac_if_available.setChecked(self.game.settings.include_jtac_if_available)
|
||||
self.include_jtac_if_available.toggled.connect(self.applySettings)
|
||||
|
||||
self.jtac_smoke_on = QCheckBox()
|
||||
self.jtac_smoke_on.setChecked(self.game.settings.jtac_smoke_on)
|
||||
self.jtac_smoke_on.toggled.connect(self.applySettings)
|
||||
|
||||
self.gameplayLayout.addWidget(QLabel("Use Supercarrier Module"), 0, 0)
|
||||
self.gameplayLayout.addWidget(self.supercarrier, 0, 1, Qt.AlignRight)
|
||||
self.gameplayLayout.addWidget(QLabel("Put Objective Markers on Map"), 1, 0)
|
||||
self.gameplayLayout.addWidget(self.generate_marks, 1, 1, Qt.AlignRight)
|
||||
self.gameplayLayout.addWidget(QLabel("Include JTAC (If available)"), 2, 0)
|
||||
self.gameplayLayout.addWidget(self.include_jtac_if_available, 2, 1, Qt.AlignRight)
|
||||
self.gameplayLayout.addWidget(QLabel("Enable JTAC smoke markers"), 3, 0)
|
||||
self.gameplayLayout.addWidget(self.jtac_smoke_on, 3, 1, Qt.AlignRight)
|
||||
|
||||
self.performance = QGroupBox("Performance")
|
||||
self.performanceLayout = QGridLayout()
|
||||
@ -263,9 +294,12 @@ class QSettingsWindow(QDialog):
|
||||
def initCheatLayout(self):
|
||||
|
||||
self.cheatPage = QWidget()
|
||||
self.cheatLayout = QGridLayout()
|
||||
self.cheatLayout = QVBoxLayout()
|
||||
self.cheatPage.setLayout(self.cheatLayout)
|
||||
|
||||
self.cheat_options = CheatSettingsBox(self.game, self.applySettings)
|
||||
self.cheatLayout.addWidget(self.cheat_options)
|
||||
|
||||
self.moneyCheatBox = QGroupBox("Money Cheat")
|
||||
self.moneyCheatBox.setAlignment(Qt.AlignTop)
|
||||
self.moneyCheatBoxLayout = QGridLayout()
|
||||
@ -281,7 +315,35 @@ class QSettingsWindow(QDialog):
|
||||
btn.setProperty("style", "btn-danger")
|
||||
btn.clicked.connect(self.cheatLambda(amount))
|
||||
self.moneyCheatBoxLayout.addWidget(btn, i/2, i%2)
|
||||
self.cheatLayout.addWidget(self.moneyCheatBox, 0, 0)
|
||||
self.cheatLayout.addWidget(self.moneyCheatBox, stretch=1)
|
||||
|
||||
def initPluginsLayout(self):
|
||||
uiPrepared = False
|
||||
row:int = 0
|
||||
for plugin in LuaPluginManager().getPlugins():
|
||||
if plugin.hasUI():
|
||||
if not uiPrepared:
|
||||
uiPrepared = True
|
||||
|
||||
self.pluginsOptionsPage = QWidget()
|
||||
self.pluginsOptionsPageLayout = QVBoxLayout()
|
||||
self.pluginsOptionsPageLayout.setAlignment(Qt.AlignTop)
|
||||
self.pluginsOptionsPage.setLayout(self.pluginsOptionsPageLayout)
|
||||
|
||||
self.pluginsPage = QWidget()
|
||||
self.pluginsPageLayout = QVBoxLayout()
|
||||
self.pluginsPageLayout.setAlignment(Qt.AlignTop)
|
||||
self.pluginsPage.setLayout(self.pluginsPageLayout)
|
||||
|
||||
self.pluginsGroup = QGroupBox("Plugins")
|
||||
self.pluginsGroupLayout = QGridLayout();
|
||||
self.pluginsGroupLayout.setAlignment(Qt.AlignTop)
|
||||
self.pluginsGroup.setLayout(self.pluginsGroupLayout)
|
||||
|
||||
self.pluginsPageLayout.addWidget(self.pluginsGroup)
|
||||
|
||||
plugin.setupUI(self, row)
|
||||
row = row + 1
|
||||
|
||||
def cheatLambda(self, amount):
|
||||
return lambda: self.cheatMoney(amount)
|
||||
@ -304,10 +366,6 @@ class QSettingsWindow(QDialog):
|
||||
self.game.settings.map_coalition_visibility = self.mapVisibiitySelection.currentData()
|
||||
self.game.settings.external_views_allowed = self.ext_views.isChecked()
|
||||
self.game.settings.generate_marks = self.generate_marks.isChecked()
|
||||
self.game.settings.include_jtac_if_available = self.include_jtac_if_available.isChecked()
|
||||
self.game.settings.jtac_smoke_on = self.jtac_smoke_on.isChecked()
|
||||
|
||||
print(self.game.settings.map_coalition_visibility)
|
||||
|
||||
self.game.settings.supercarrier = self.supercarrier.isChecked()
|
||||
|
||||
@ -322,6 +380,8 @@ class QSettingsWindow(QDialog):
|
||||
self.game.settings.perf_culling = self.culling.isChecked()
|
||||
self.game.settings.perf_culling_distance = int(self.culling_distance.value())
|
||||
|
||||
self.game.settings.show_red_ato = self.cheat_options.show_red_ato
|
||||
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
|
||||
def onSelectionChanged(self):
|
||||
|
||||
BIN
resources/plugins/_doc/0.png
Normal file
BIN
resources/plugins/_doc/0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
resources/plugins/_doc/1.png
Normal file
BIN
resources/plugins/_doc/1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
resources/plugins/_doc/2.png
Normal file
BIN
resources/plugins/_doc/2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
84
resources/plugins/_doc/plugins_readme.md
Normal file
84
resources/plugins/_doc/plugins_readme.md
Normal file
@ -0,0 +1,84 @@
|
||||
# LUA Plugin system
|
||||
|
||||
This plugin system was made for injecting LUA scripts in dcs-liberation missions.
|
||||
|
||||
The resources for the plugins are stored in the `resources/plugins` folder ; each plugin has its own folder.
|
||||
|
||||
## How does the system work ?
|
||||
|
||||
The application first reads the `resources/plugins/plugins.json` file to get a list of plugins to load, in order.
|
||||
Each entry in this list should correspond to a subfolder of the `resources/plugins` directory, where a `plugin.json` file exists.
|
||||
This file is the description of the plugin.
|
||||
|
||||
### plugin.json
|
||||
|
||||
The *base* and *jtacautolase* plugins both are included in the standard dcs-liberation distribution.
|
||||
You can check their respective `plugin.json` files to understand how they work.
|
||||
Here's a quick rundown of the file's components :
|
||||
|
||||
- `mnemonic` : the short, technical name of the plugin. It's the name of the folder, and the name of the plugin in the application's settings
|
||||
- `skipUI` : if *true*, this plugin will not appear in the plugins selection user interface. Useful to force a plugin ON or OFF (see the *base* plugin)
|
||||
- `nameInUI` : the title of the plugin as it will appear in the plugins selection user interface.
|
||||
- `defaultValue` : the selection value of the plugin, when first installed ; if true, plugin is selected.
|
||||
- `specificOptions` : a list of specific plugin options
|
||||
- `nameInUI` : the title of the option as it will appear in the plugins specific options user interface.
|
||||
- `mnemonic` : the short, technical name of the option. It's the name of the LUA variable passed to the configuration script, and the name of the option in the application's settings
|
||||
- `defaultValue` : the selection value of the option, when first installed ; if true, option is selected.
|
||||
- `scriptsWorkOrders` : a list of work orders that can be used to load or disable loading a specific LUA script
|
||||
- `file` : the name of the LUA file in the plugin folder.
|
||||
- `mnemonic` : the technical name of the LUA component. The filename may be more precise than needed (e.g. include a version number) ; this is used to load each file only once, and also to disable loading a file
|
||||
- `disable` : if true, the script will be disabled instead of loaded
|
||||
- `configurationWorkOrders` : a list of work orders that can be used to load a configuration LUA script (same description as above)
|
||||
|
||||
## Standard plugins
|
||||
|
||||
### The *base* plugin
|
||||
|
||||
The *base* plugin contains the scripts that are going to be injected in every dcs-liberation missions.
|
||||
It is mandatory.
|
||||
|
||||
### The *JTACAutolase* plugin
|
||||
|
||||
This plugin replaces the vanilla JTAC functionality in dcs-liberation.
|
||||
|
||||
### The *VEAF framework* plugin
|
||||
|
||||
When enabled, this plugin will inject and configure the VEAF Framework scripts in the mission.
|
||||
|
||||
These scripts add a lot of runtime functionalities :
|
||||
|
||||
- spawning of units and groups (and portable TACANs)
|
||||
- air-to-ground missions
|
||||
- air-to-air missions
|
||||
- transport missions
|
||||
- carrier operations (not Moose)
|
||||
- tanker move
|
||||
- weather and ATC
|
||||
- shelling a zone, lighting it up
|
||||
- managing assets (tankers, awacs, aircraft carriers) : getting info, state, respawning them if needed
|
||||
- managing named points (position, info, ATC)
|
||||
- managing a dynamic radio menu
|
||||
- managing remote calls to the mission through NIOD (RPC) and SLMOD (LUA sockets)
|
||||
- managing security (not allowing everyone to do every action)
|
||||
- define groups templates
|
||||
|
||||
You can find the *VEAF Framework* plugin [on GitHub](https://github.com/VEAF/dcs-liberation-veaf-framework/releases)
|
||||
For more information, please visit the [VEAF Framework documentation site](https://veaf.github.io/VEAF-Mission-Creation-Tools/) (work in progress)
|
||||
|
||||
## Custom plugins
|
||||
|
||||
The easiest way to create a custom plugin is to copy an existing plugin, and modify the files.
|
||||
|
||||
## New settings pages
|
||||
|
||||

|
||||
|
||||
Custom plugins can be enabled or disabled in the new *LUA Plugins* settings page.
|
||||
|
||||

|
||||
|
||||
For plugins which expose specific options (such as "use smoke" for the *JTACAutoLase* plugin), the *LUA Plugins Options* settings page lists these options.
|
||||
|
||||

|
||||
|
||||
|
||||
@ -39,46 +39,54 @@ write_state = function()
|
||||
-- messageAll("Done writing DCS Liberation state.")
|
||||
end
|
||||
|
||||
debriefing_file_location = nil
|
||||
if dcsLiberation then
|
||||
debriefing_file_location = dcsLiberation.installPath
|
||||
end
|
||||
if debriefing_file_location then
|
||||
logger:info("Using DCS Liberation install folder for state.json")
|
||||
else
|
||||
|
||||
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
|
||||
-- 1. Environment variable mode, to support dedicated server hosting
|
||||
-- 2. Embedded DCS Liberation Generation, to support locally hosted single player
|
||||
-- 3. Retain the classic TEMP directory logic
|
||||
|
||||
if os then
|
||||
debriefing_file_location = os.getenv("LIBERATION_EXPORT_DIR")
|
||||
if debriefing_file_location then debriefing_file_location = debriefing_file_location .. "\\" end
|
||||
end
|
||||
if debriefing_file_location then
|
||||
logger:info("Using LIBERATION_EXPORT_DIR environment variable for state.json")
|
||||
else
|
||||
if os then
|
||||
debriefing_file_location = os.getenv("TEMP")
|
||||
if debriefing_file_location then debriefing_file_location = debriefing_file_location .. "\\" end
|
||||
end
|
||||
if debriefing_file_location then
|
||||
logger:info("Using TEMP environment variable for state.json")
|
||||
else
|
||||
if lfs then
|
||||
debriefing_file_location = lfs.writedir()
|
||||
end
|
||||
if debriefing_file_location then
|
||||
logger:info("Using DCS working directory for state.json")
|
||||
end
|
||||
local exportDirectory = os.getenv("LIBERATION_EXPORT_DIR")
|
||||
|
||||
if exportDirectory then
|
||||
logger:info("Using LIBERATION_EXPORT_DIR to locate the state.json")
|
||||
local useCurrentStamping = os.getenv("LIBERATION_EXPORT_STAMPED_STATE")
|
||||
exportDirectory = exportDirectory .. "\\"
|
||||
return insertFileName(exportDirectory, useCurrentStamping)
|
||||
end
|
||||
end
|
||||
end
|
||||
if debriefing_file_location then
|
||||
local filename = "state.json"
|
||||
if not debriefing_file_location:sub(-#filename) == filename then
|
||||
debriefing_file_location = debriefing_file_location .. filename
|
||||
|
||||
if dcsLiberation then
|
||||
logger:info("Using DCS Liberation install folder for state.json")
|
||||
return insertFileName(dcsLiberation.installPath)
|
||||
end
|
||||
|
||||
if lfs then
|
||||
logger:info("Using DCS working directory for state.json")
|
||||
return insertFileName(lfs.writedir())
|
||||
end
|
||||
logger:info(string.format("DCS Liberation state will be written as json to [[%s]]",debriefing_file_location))
|
||||
else
|
||||
logger:error("No usable storage path for state.json")
|
||||
end
|
||||
|
||||
|
||||
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()
|
||||
if pcall(write_state) then
|
||||
-- messageAll("Written DCS Liberation state to "..debriefing_file_location)
|
||||
22
resources/plugins/base/plugin.json
Normal file
22
resources/plugins/base/plugin.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"mnemonic": "base",
|
||||
"skipUI": true,
|
||||
"nameInUI": "",
|
||||
"defaultValue": true,
|
||||
"specificOptions": [],
|
||||
"scriptsWorkOrders": [
|
||||
{
|
||||
"file": "mist_4_3_74.lua",
|
||||
"mnemonic": "mist"
|
||||
},
|
||||
{
|
||||
"file": "json.lua",
|
||||
"mnemonic": "json"
|
||||
},
|
||||
{
|
||||
"file": "dcs_liberation.lua",
|
||||
"mnemonic": "liberation"
|
||||
}
|
||||
],
|
||||
"configurationWorkOrders": []
|
||||
}
|
||||
37
resources/plugins/jtacautolase/jtacautolase-config.lua
Normal file
37
resources/plugins/jtacautolase/jtacautolase-config.lua
Normal file
@ -0,0 +1,37 @@
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- configuration file for the JTAC Autolase framework
|
||||
--
|
||||
-- This configuration is tailored for a mission generated by DCS Liberation
|
||||
-- see https://github.com/Khopa/dcs_liberation
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- JTACAutolase plugin - configuration
|
||||
env.info("DCSLiberation|JTACAutolase plugin - configuration")
|
||||
|
||||
if dcsLiberation then
|
||||
env.info(string.format("DCSLiberation|JTACAutolase plugin - dcsLiberation"))
|
||||
|
||||
-- specific options
|
||||
local smoke = false
|
||||
|
||||
-- retrieve specific options values
|
||||
if dcsLiberation.plugins then
|
||||
env.info(string.format("DCSLiberation|JTACAutolase plugin - dcsLiberation.plugins"))
|
||||
|
||||
if dcsLiberation.plugins.jtacautolase then
|
||||
env.info(string.format("DCSLiberation|JTACAutolase plugin - dcsLiberation.plugins.jtacautolase"))
|
||||
smoke = dcsLiberation.plugins.jtacautolase.smoke
|
||||
env.info(string.format("DCSLiberation|JTACAutolase plugin - smoke = %s",tostring(smoke)))
|
||||
end
|
||||
end
|
||||
|
||||
-- actual configuration code
|
||||
for _, jtac in pairs(dcsLiberation.JTACs) do
|
||||
env.info(string.format("DCSLiberation|JTACAutolase plugin - setting up %s",jtac.dcsUnit))
|
||||
if JTACAutoLase then
|
||||
env.info(string.format("DCSLiberation|JTACAutolase plugin - calling dcsLiberation.JTACAutoLase"))
|
||||
JTACAutoLase(jtac.dcsUnit, jtac.laserCode, smoke, 'vehicle')
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
6822
resources/plugins/jtacautolase/mist_4_3_74.lua
Normal file
6822
resources/plugins/jtacautolase/mist_4_3_74.lua
Normal file
File diff suppressed because it is too large
Load Diff
28
resources/plugins/jtacautolase/plugin.json
Normal file
28
resources/plugins/jtacautolase/plugin.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"mnemonic": "jtacautolase",
|
||||
"nameInUI": "JTAC Autolase",
|
||||
"defaultValue": true,
|
||||
"specificOptions": [
|
||||
{
|
||||
"nameInUI": "Use smoke",
|
||||
"mnemonic": "smoke",
|
||||
"defaultValue": true
|
||||
}
|
||||
],
|
||||
"scriptsWorkOrders": [
|
||||
{
|
||||
"file": "mist_4_3_74.lua",
|
||||
"mnemonic": "mist"
|
||||
},
|
||||
{
|
||||
"file": "JTACAutoLase.lua",
|
||||
"mnemonic": "jtacautolase-script"
|
||||
}
|
||||
],
|
||||
"configurationWorkOrders": [
|
||||
{
|
||||
"file": "jtacautolase-config.lua",
|
||||
"mnemonic": "jtacautolase-config"
|
||||
}
|
||||
]
|
||||
}
|
||||
5
resources/plugins/plugins.json
Normal file
5
resources/plugins/plugins.json
Normal file
@ -0,0 +1,5 @@
|
||||
[
|
||||
"veaf",
|
||||
"jtacautolase",
|
||||
"base"
|
||||
]
|
||||
@ -1,29 +0,0 @@
|
||||
# this is a list of lua scripts that will be injected in the mission, in the same order
|
||||
mist.lua
|
||||
Moose.lua
|
||||
CTLD.lua
|
||||
NIOD.lua
|
||||
WeatherMark.lua
|
||||
veaf.lua
|
||||
dcsUnits.lua
|
||||
# JTACAutoLase is an empty file, only there to disable loading the official script (already included in CTLD)
|
||||
JTACAutoLase.lua
|
||||
veafAssets.lua
|
||||
veafCarrierOperations.lua
|
||||
veafCarrierOperations2.lua
|
||||
veafCasMission.lua
|
||||
veafCombatMission.lua
|
||||
veafCombatZone.lua
|
||||
veafGrass.lua
|
||||
veafInterpreter.lua
|
||||
veafMarkers.lua
|
||||
veafMove.lua
|
||||
veafNamedPoints.lua
|
||||
veafRadio.lua
|
||||
veafRemote.lua
|
||||
veafSecurity.lua
|
||||
veafShortcuts.lua
|
||||
veafSpawn.lua
|
||||
veafTransportMission.lua
|
||||
veafUnits.lua
|
||||
missionConfig.lua
|
||||
BIN
resources/ui/misc/light/plugins.png
Normal file
BIN
resources/ui/misc/light/plugins.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
resources/ui/misc/light/pluginsoptions.png
Normal file
BIN
resources/ui/misc/light/pluginsoptions.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Loading…
x
Reference in New Issue
Block a user