diff --git a/.gitignore b/.gitignore
index 1bf595f6..26831339 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,4 +21,3 @@ logs/
qt_ui/logs/liberation.log
*.psd
-resources/scripts/plugins/*
diff --git a/game/event/event.py b/game/event/event.py
index 0af3852c..8f7ac1b8 100644
--- a/game/event/event.py
+++ b/game/event/event.py
@@ -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):
diff --git a/game/game.py b/game/game.py
index dcac8220..bd0f68f7 100644
--- a/game/game.py
+++ b/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):
diff --git a/game/inventory.py b/game/inventory.py
index 5ef68b04..ae75c837 100644
--- a/game/inventory.py
+++ b/game/inventory.py
@@ -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()
diff --git a/game/operation/operation.py b/game/operation/operation.py
index ecc82e51..c83f903f 100644
--- a/game/operation/operation.py
+++ b/game/operation/operation.py
@@ -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)
diff --git a/game/settings.py b/game/settings.py
index 4566ad0f..11ba0753 100644
--- a/game/settings.py
+++ b/game/settings.py
@@ -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)
diff --git a/game/weather.py b/game/weather.py
new file mode 100644
index 00000000..d6775614
--- /dev/null
+++ b/game/weather.py
@@ -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()
diff --git a/gen/aircraft.py b/gen/aircraft.py
index 49c6e8cd..5076ee8e 100644
--- a/gen/aircraft.py
+++ b/gen/aircraft.py
@@ -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
diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py
index 8a98dba7..97aeea1f 100644
--- a/gen/airsupportgen.py
+++ b/gen/airsupportgen.py
@@ -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")
\ No newline at end of file
diff --git a/gen/armor.py b/gen/armor.py
index 426dc05a..2873002a 100644
--- a/gen/armor.py
+++ b/gen/armor.py
@@ -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):
diff --git a/gen/briefinggen.py b/gen/briefinggen.py
index d52f25cc..1eef67a7 100644
--- a/gen/briefinggen.py
+++ b/gen/briefinggen.py
@@ -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
diff --git a/gen/environmentgen.py b/gen/environmentgen.py
index 57d70452..7712cea5 100644
--- a/gen/environmentgen.py
+++ b/gen/environmentgen.py
@@ -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)
diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py
index 01f5d1b4..0ea53eb6 100644
--- a/gen/flights/ai_flight_planner.py
+++ b/gen/flights/ai_flight_planner.py
@@ -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]
diff --git a/gen/flights/ai_flight_planner_db.py b/gen/flights/ai_flight_planner_db.py
index 715a7f66..2baadf7d 100644
--- a/gen/flights/ai_flight_planner_db.py
+++ b/gen/flights/ai_flight_planner_db.py
@@ -326,7 +326,7 @@ SEAD_CAPABLE = [
F_4E,
FA_18C_hornet,
F_15E,
- # F_16C_50, Not yet
+ F_16C_50,
AV8BNA,
JF_17,
diff --git a/gen/flights/flight.py b/gen/flights/flight.py
index c9031bb4..82a25226 100644
--- a/gen/flights/flight.py
+++ b/gen/flights/flight.py
@@ -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
diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py
index 74462c2d..ed2b7bb0 100644
--- a/gen/flights/flightplan.py
+++ b/gen/flights/flightplan.py
@@ -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.
diff --git a/gen/flights/traveltime.py b/gen/flights/traveltime.py
index 87d2817d..a4690f1a 100644
--- a/gen/flights/traveltime.py
+++ b/gen/flights/traveltime.py
@@ -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,
diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py
index a0374d25..fd4b5aed 100644
--- a/gen/flights/waypointbuilder.py
+++ b/gen/flights/waypointbuilder.py
@@ -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.
diff --git a/gen/kneeboard.py b/gen/kneeboard.py
index cea2e591..a0c4c7a5 100644
--- a/gen/kneeboard.py
+++ b/gen/kneeboard.py
@@ -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 = []
diff --git a/plugin/__init__.py b/plugin/__init__.py
new file mode 100644
index 00000000..37f3c6d4
--- /dev/null
+++ b/plugin/__init__.py
@@ -0,0 +1,2 @@
+from .luaplugin import LuaPlugin
+from .manager import LuaPluginManager
\ No newline at end of file
diff --git a/plugin/luaplugin.py b/plugin/luaplugin.py
new file mode 100644
index 00000000..7bc4f57a
--- /dev/null
+++ b/plugin/luaplugin.py
@@ -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
\ No newline at end of file
diff --git a/plugin/manager.py b/plugin/manager.py
new file mode 100644
index 00000000..d7625821
--- /dev/null
+++ b/plugin/manager.py
@@ -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
\ No newline at end of file
diff --git a/qt_ui/models.py b/qt_ui/models.py
index ba816fd1..6480d933 100644
--- a/qt_ui/models.py
+++ b/qt_ui/models.py
@@ -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)
diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py
index 1683ddb8..fcf8ef32 100644
--- a/qt_ui/uiconstants.py
+++ b/qt_ui/uiconstants.py
@@ -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")
diff --git a/qt_ui/widgets/QTopPanel.py b/qt_ui/widgets/QTopPanel.py
index 99f0ac9f..dadbee0d 100644
--- a/qt_ui/widgets/QTopPanel.py
+++ b/qt_ui/widgets/QTopPanel.py
@@ -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 = '
'.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:
"
+ "
"
+ f"{formatted}
"
+ "
"
+ "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.
"
+ "
"
+ "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
diff --git a/qt_ui/widgets/QTurnCounter.py b/qt_ui/widgets/QTurnCounter.py
index f7e6fd88..a26112e1 100644
--- a/qt_ui/widgets/QTurnCounter.py
+++ b/qt_ui/widgets/QTurnCounter.py
@@ -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))
diff --git a/qt_ui/widgets/ato.py b/qt_ui/widgets/ato.py
index 32178381..bc45fac9 100644
--- a/qt_ui/widgets/ato.py
+++ b/qt_ui/widgets/ato.py
@@ -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)
diff --git a/qt_ui/widgets/combos/QFlightTypeComboBox.py b/qt_ui/widgets/combos/QFlightTypeComboBox.py
index 9577b26c..429ff902 100644
--- a/qt_ui/widgets/combos/QFlightTypeComboBox.py
+++ b/qt_ui/widgets/combos/QFlightTypeComboBox.py
@@ -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
diff --git a/qt_ui/widgets/combos/QOriginAirfieldSelector.py b/qt_ui/widgets/combos/QOriginAirfieldSelector.py
index b0995a6b..904ec114 100644
--- a/qt_ui/widgets/combos/QOriginAirfieldSelector.py
+++ b/qt_ui/widgets/combos/QOriginAirfieldSelector.py
@@ -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)
diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py
index 20f9e151..567bce80 100644
--- a/qt_ui/widgets/map/QLiberationMap.py
+++ b/qt_ui/widgets/map/QLiberationMap.py
@@ -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)
diff --git a/qt_ui/widgets/map/QMapGroundObject.py b/qt_ui/widgets/map/QMapGroundObject.py
index af0789a8..a7d857f3 100644
--- a/qt_ui/widgets/map/QMapGroundObject.py
+++ b/qt_ui/widgets/map/QMapGroundObject.py
@@ -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"
diff --git a/qt_ui/windows/GameUpdateSignal.py b/qt_ui/windows/GameUpdateSignal.py
index 3c112952..529a7498 100644
--- a/qt_ui/windows/GameUpdateSignal.py
+++ b/qt_ui/windows/GameUpdateSignal.py
@@ -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
diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py
index db5caa10..99783d11 100644
--- a/qt_ui/windows/QLiberationWindow.py
+++ b/qt_ui/windows/QLiberationWindow.py
@@ -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 = "
DCS Liberation " + CONST.VERSION_STRING + "
" + \
diff --git a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py
index b679bf7b..2dbba2f3 100644
--- a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py
+++ b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py
@@ -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)
diff --git a/qt_ui/windows/mission/QPackageDialog.py b/qt_ui/windows/mission/QPackageDialog.py
index 3c64c160..4ee1a0a1 100644
--- a/qt_ui/windows/mission/QPackageDialog.py
+++ b/qt_ui/windows/mission/QPackageDialog.py
@@ -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.
diff --git a/qt_ui/windows/mission/flight/QFlightCreator.py b/qt_ui/windows/mission/flight/QFlightCreator.py
index e514b9d7..a2ca14ee 100644
--- a/qt_ui/windows/mission/flight/QFlightCreator.py
+++ b/qt_ui/windows/mission/flight/QFlightCreator.py
@@ -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)
+ )
diff --git a/qt_ui/windows/mission/flight/QFlightPlanner.py b/qt_ui/windows/mission/flight/QFlightPlanner.py
index af48219c..b4eb9b36 100644
--- a/qt_ui/windows/mission/flight/QFlightPlanner.py
+++ b/qt_ui/windows/mission/flight/QFlightPlanner.py
@@ -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")
diff --git a/qt_ui/windows/mission/flight/settings/QFlightDepartureDisplay.py b/qt_ui/windows/mission/flight/settings/QFlightDepartureDisplay.py
new file mode 100644
index 00000000..a720cc8b
--- /dev/null
+++ b/qt_ui/windows/mission/flight/settings/QFlightDepartureDisplay.py
@@ -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 {flight.from_cp.name}"
+ ))
+ 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)
diff --git a/qt_ui/windows/mission/flight/settings/QFlightDepartureEditor.py b/qt_ui/windows/mission/flight/settings/QFlightDepartureEditor.py
deleted file mode 100644
index abf429cf..00000000
--- a/qt_ui/windows/mission/flight/settings/QFlightDepartureEditor.py
+++ /dev/null
@@ -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 " + self.flight.from_cp.name + "")
- 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)
diff --git a/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py b/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py
index 99f2b63f..f1419669 100644
--- a/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py
+++ b/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py
@@ -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))
diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py
index 98064b5c..21a85a84 100644
--- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py
+++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py
@@ -54,6 +54,7 @@ class QFlightWaypointTab(QFrame):
rlayout.addWidget(QLabel("Generator :"))
rlayout.addWidget(QLabel("AI compatible"))
+ # 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()
diff --git a/qt_ui/windows/settings/QSettingsWindow.py b/qt_ui/windows/settings/QSettingsWindow.py
index 6ccdb226..1f1053ed 100644
--- a/qt_ui/windows/settings/QSettingsWindow.py
+++ b/qt_ui/windows/settings/QSettingsWindow.py
@@ -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):
diff --git a/resources/plugins/_doc/0.png b/resources/plugins/_doc/0.png
new file mode 100644
index 00000000..3320b908
Binary files /dev/null and b/resources/plugins/_doc/0.png differ
diff --git a/resources/plugins/_doc/1.png b/resources/plugins/_doc/1.png
new file mode 100644
index 00000000..069c45db
Binary files /dev/null and b/resources/plugins/_doc/1.png differ
diff --git a/resources/plugins/_doc/2.png b/resources/plugins/_doc/2.png
new file mode 100644
index 00000000..97952fde
Binary files /dev/null and b/resources/plugins/_doc/2.png differ
diff --git a/resources/plugins/_doc/plugins_readme.md b/resources/plugins/_doc/plugins_readme.md
new file mode 100644
index 00000000..7df75d91
--- /dev/null
+++ b/resources/plugins/_doc/plugins_readme.md
@@ -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.
+
+
+
+
diff --git a/resources/scripts/dcs_liberation.lua b/resources/plugins/base/dcs_liberation.lua
similarity index 71%
rename from resources/scripts/dcs_liberation.lua
rename to resources/plugins/base/dcs_liberation.lua
index 70a5c239..8017b1f5 100644
--- a/resources/scripts/dcs_liberation.lua
+++ b/resources/plugins/base/dcs_liberation.lua
@@ -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)
diff --git a/resources/scripts/json.lua b/resources/plugins/base/json.lua
similarity index 100%
rename from resources/scripts/json.lua
rename to resources/plugins/base/json.lua
diff --git a/resources/scripts/mist_4_3_74.lua b/resources/plugins/base/mist_4_3_74.lua
similarity index 100%
rename from resources/scripts/mist_4_3_74.lua
rename to resources/plugins/base/mist_4_3_74.lua
diff --git a/resources/plugins/base/plugin.json b/resources/plugins/base/plugin.json
new file mode 100644
index 00000000..2234980e
--- /dev/null
+++ b/resources/plugins/base/plugin.json
@@ -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": []
+}
\ No newline at end of file
diff --git a/resources/scripts/JTACAutoLase.lua b/resources/plugins/jtacautolase/JTACAutoLase.lua
similarity index 100%
rename from resources/scripts/JTACAutoLase.lua
rename to resources/plugins/jtacautolase/JTACAutoLase.lua
diff --git a/resources/plugins/jtacautolase/jtacautolase-config.lua b/resources/plugins/jtacautolase/jtacautolase-config.lua
new file mode 100644
index 00000000..47a88edf
--- /dev/null
+++ b/resources/plugins/jtacautolase/jtacautolase-config.lua
@@ -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
\ No newline at end of file
diff --git a/resources/plugins/jtacautolase/mist_4_3_74.lua b/resources/plugins/jtacautolase/mist_4_3_74.lua
new file mode 100644
index 00000000..ffb822a4
--- /dev/null
+++ b/resources/plugins/jtacautolase/mist_4_3_74.lua
@@ -0,0 +1,6822 @@
+--[[--
+MIST Mission Scripting Tools.
+## Description:
+MIssion Scripting Tools (MIST) is a collection of Lua functions
+and databases that is intended to be a supplement to the standard
+Lua functions included in the simulator scripting engine.
+
+MIST functions and databases provide ready-made solutions to many common
+scripting tasks and challenges, enabling easier scripting and saving
+mission scripters time. The table mist.flagFuncs contains a set of
+Lua functions (that are similar to Slmod functions) that do not
+require detailed Lua knowledge to use.
+
+However, the majority of MIST does require knowledge of the Lua language,
+and, if you are going to utilize these components of MIST, it is necessary
+that you read the Simulator Scripting Engine guide on the official ED wiki.
+
+## Links:
+
+ED Forum Thread:
+
+##Github:
+
+Development
+
+Official Releases
+
+@script MIST
+@author Speed
+@author Grimes
+@author lukrop
+]]
+mist = {}
+
+-- don't change these
+mist.majorVersion = 4
+mist.minorVersion = 3
+mist.build = 74
+
+-- forward declaration of log shorthand
+local log
+
+do -- the main scope
+ local coroutines = {}
+
+ local tempSpawnedUnits = {} -- birth events added here
+ local tempSpawnedGroups = {}
+ local tempSpawnGroupsCounter = 0
+
+ local mistAddedObjects = {} -- mist.dynAdd unit data added here
+ local mistAddedGroups = {} -- mist.dynAdd groupdata added here
+ local writeGroups = {}
+ local lastUpdateTime = 0
+
+ local updateAliveUnitsCounter = 0
+ local updateTenthSecond = 0
+
+ local mistGpId = 7000
+ local mistUnitId = 7000
+ local mistDynAddIndex = {[' air '] = 0, [' hel '] = 0, [' gnd '] = 0, [' bld '] = 0, [' static '] = 0, [' shp '] = 0}
+
+ local scheduledTasks = {}
+ local taskId = 0
+ local idNum = 0
+
+ mist.nextGroupId = 1
+ mist.nextUnitId = 1
+
+ local dbLog
+
+ local function initDBs() -- mist.DBs scope
+ mist.DBs = {}
+
+ mist.DBs.missionData = {}
+ if env.mission then
+
+ mist.DBs.missionData.startTime = env.mission.start_time
+ mist.DBs.missionData.theatre = env.mission.theatre
+ mist.DBs.missionData.version = env.mission.version
+ mist.DBs.missionData.files = {}
+ if type(env.mission.resourceCounter) == 'table' then
+ for fIndex, fData in pairs (env.mission.resourceCounter) do
+ mist.DBs.missionData.files[#mist.DBs.missionData.files + 1] = mist.utils.deepCopy(fIndex)
+ end
+ end
+ -- if we add more coalition specific data then bullsye should be categorized by coaliton. For now its just the bullseye table
+ mist.DBs.missionData.bullseye = {red = {}, blue = {}}
+ mist.DBs.missionData.bullseye.red.x = env.mission.coalition.red.bullseye.x --should it be point.x?
+ mist.DBs.missionData.bullseye.red.y = env.mission.coalition.red.bullseye.y
+ mist.DBs.missionData.bullseye.blue.x = env.mission.coalition.blue.bullseye.x
+ mist.DBs.missionData.bullseye.blue.y = env.mission.coalition.blue.bullseye.y
+ end
+
+ mist.DBs.zonesByName = {}
+ mist.DBs.zonesByNum = {}
+
+
+ if env.mission.triggers and env.mission.triggers.zones then
+ for zone_ind, zone_data in pairs(env.mission.triggers.zones) do
+ if type(zone_data) == 'table' then
+ local zone = mist.utils.deepCopy(zone_data)
+ zone.point = {} -- point is used by SSE
+ zone.point.x = zone_data.x
+ zone.point.y = 0
+ zone.point.z = zone_data.y
+
+ mist.DBs.zonesByName[zone_data.name] = zone
+ mist.DBs.zonesByNum[#mist.DBs.zonesByNum + 1] = mist.utils.deepCopy(zone) --[[deepcopy so that the zone in zones_by_name and the zone in
+ zones_by_num se are different objects.. don't want them linked.]]
+ end
+ end
+ end
+
+ mist.DBs.navPoints = {}
+ mist.DBs.units = {}
+ --Build mist.db.units and mist.DBs.navPoints
+ for coa_name, coa_data in pairs(env.mission.coalition) do
+
+ if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then
+ mist.DBs.units[coa_name] = {}
+
+ -- build nav points DB
+ mist.DBs.navPoints[coa_name] = {}
+ if coa_data.nav_points then --navpoints
+ --mist.debug.writeData (mist.utils.serialize,{'NavPoints',coa_data.nav_points}, 'NavPoints.txt')
+ for nav_ind, nav_data in pairs(coa_data.nav_points) do
+
+ if type(nav_data) == 'table' then
+ mist.DBs.navPoints[coa_name][nav_ind] = mist.utils.deepCopy(nav_data)
+
+ mist.DBs.navPoints[coa_name][nav_ind].name = nav_data.callsignStr -- name is a little bit more self-explanatory.
+ mist.DBs.navPoints[coa_name][nav_ind].point = {} -- point is used by SSE, support it.
+ mist.DBs.navPoints[coa_name][nav_ind].point.x = nav_data.x
+ mist.DBs.navPoints[coa_name][nav_ind].point.y = 0
+ mist.DBs.navPoints[coa_name][nav_ind].point.z = nav_data.y
+ end
+ end
+ end
+ if coa_data.country then --there is a country table
+ for cntry_id, cntry_data in pairs(coa_data.country) do
+
+ local countryName = string.lower(cntry_data.name)
+ mist.DBs.units[coa_name][countryName] = {}
+ mist.DBs.units[coa_name][countryName].countryId = cntry_data.id
+
+ if type(cntry_data) == 'table' then --just making sure
+
+ for obj_type_name, obj_type_data in pairs(cntry_data) do
+
+ if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check
+
+ local category = obj_type_name
+
+ if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group!
+
+ mist.DBs.units[coa_name][countryName][category] = {}
+
+ for group_num, group_data in pairs(obj_type_data.group) do
+
+ if group_data and group_data.units and type(group_data.units) == 'table' then --making sure again- this is a valid group
+
+ mist.DBs.units[coa_name][countryName][category][group_num] = {}
+ local groupName = group_data.name
+ if env.mission.version > 7 then
+ groupName = env.getValueDictByKey(groupName)
+ end
+ mist.DBs.units[coa_name][countryName][category][group_num].groupName = groupName
+ mist.DBs.units[coa_name][countryName][category][group_num].groupId = group_data.groupId
+ mist.DBs.units[coa_name][countryName][category][group_num].category = category
+ mist.DBs.units[coa_name][countryName][category][group_num].coalition = coa_name
+ mist.DBs.units[coa_name][countryName][category][group_num].country = countryName
+ mist.DBs.units[coa_name][countryName][category][group_num].countryId = cntry_data.id
+ mist.DBs.units[coa_name][countryName][category][group_num].startTime = group_data.start_time
+ mist.DBs.units[coa_name][countryName][category][group_num].task = group_data.task
+ mist.DBs.units[coa_name][countryName][category][group_num].hidden = group_data.hidden
+
+ mist.DBs.units[coa_name][countryName][category][group_num].units = {}
+
+ mist.DBs.units[coa_name][countryName][category][group_num].radioSet = group_data.radioSet
+ mist.DBs.units[coa_name][countryName][category][group_num].uncontrolled = group_data.uncontrolled
+ mist.DBs.units[coa_name][countryName][category][group_num].frequency = group_data.frequency
+ mist.DBs.units[coa_name][countryName][category][group_num].modulation = group_data.modulation
+
+ for unit_num, unit_data in pairs(group_data.units) do
+ local units_tbl = mist.DBs.units[coa_name][countryName][category][group_num].units --pointer to the units table for this group
+
+ units_tbl[unit_num] = {}
+ if env.mission.version > 7 then
+ units_tbl[unit_num].unitName = env.getValueDictByKey(unit_data.name)
+ else
+ units_tbl[unit_num].unitName = unit_data.name
+ end
+ units_tbl[unit_num].type = unit_data.type
+ units_tbl[unit_num].skill = unit_data.skill --will be nil for statics
+ units_tbl[unit_num].unitId = unit_data.unitId
+ units_tbl[unit_num].category = category
+ units_tbl[unit_num].coalition = coa_name
+ units_tbl[unit_num].country = countryName
+ units_tbl[unit_num].countryId = cntry_data.id
+ units_tbl[unit_num].heading = unit_data.heading
+ units_tbl[unit_num].playerCanDrive = unit_data.playerCanDrive
+ units_tbl[unit_num].alt = unit_data.alt
+ units_tbl[unit_num].alt_type = unit_data.alt_type
+ units_tbl[unit_num].speed = unit_data.speed
+ units_tbl[unit_num].livery_id = unit_data.livery_id
+ if unit_data.point then --ME currently does not work like this, but it might one day
+ units_tbl[unit_num].point = unit_data.point
+ else
+ units_tbl[unit_num].point = {}
+ units_tbl[unit_num].point.x = unit_data.x
+ units_tbl[unit_num].point.y = unit_data.y
+ end
+ units_tbl[unit_num].x = unit_data.x
+ units_tbl[unit_num].y = unit_data.y
+
+ units_tbl[unit_num].callsign = unit_data.callsign
+ units_tbl[unit_num].onboard_num = unit_data.onboard_num
+ units_tbl[unit_num].hardpoint_racks = unit_data.hardpoint_racks
+ units_tbl[unit_num].psi = unit_data.psi
+
+
+ units_tbl[unit_num].groupName = groupName
+ units_tbl[unit_num].groupId = group_data.groupId
+
+ if unit_data.AddPropAircraft then
+ units_tbl[unit_num].AddPropAircraft = unit_data.AddPropAircraft
+ end
+
+ if category == 'static' then
+ units_tbl[unit_num].categoryStatic = unit_data.category
+ units_tbl[unit_num].shape_name = unit_data.shape_name
+ if unit_data.mass then
+ units_tbl[unit_num].mass = unit_data.mass
+ end
+
+ if unit_data.canCargo then
+ units_tbl[unit_num].canCargo = unit_data.canCargo
+ end
+ end
+
+ end --for unit_num, unit_data in pairs(group_data.units) do
+ end --if group_data and group_data.units then
+ end --for group_num, group_data in pairs(obj_type_data.group) do
+ end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then
+ end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then
+ end --for obj_type_name, obj_type_data in pairs(cntry_data) do
+ end --if type(cntry_data) == 'table' then
+ end --for cntry_id, cntry_data in pairs(coa_data.country) do
+ end --if coa_data.country then --there is a country table
+ end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then
+ end --for coa_name, coa_data in pairs(mission.coalition) do
+
+ mist.DBs.unitsByName = {}
+ mist.DBs.unitsById = {}
+ mist.DBs.unitsByCat = {}
+
+ mist.DBs.unitsByCat.helicopter = {} -- adding default categories
+ mist.DBs.unitsByCat.plane = {}
+ mist.DBs.unitsByCat.ship = {}
+ mist.DBs.unitsByCat.static = {}
+ mist.DBs.unitsByCat.vehicle = {}
+
+ mist.DBs.unitsByNum = {}
+
+ mist.DBs.groupsByName = {}
+ mist.DBs.groupsById = {}
+ mist.DBs.humansByName = {}
+ mist.DBs.humansById = {}
+
+ mist.DBs.dynGroupsAdded = {} -- will be filled by mist.dbUpdate from dynamically spawned groups
+ mist.DBs.activeHumans = {}
+
+ mist.DBs.aliveUnits = {} -- will be filled in by the "updateAliveUnits" coroutine in mist.main.
+
+ mist.DBs.removedAliveUnits = {} -- will be filled in by the "updateAliveUnits" coroutine in mist.main.
+
+ mist.DBs.const = {}
+
+ -- not accessible by SSE, must use static list :-/
+ mist.DBs.const.callsigns = {
+ ['NATO'] = {
+ ['rules'] = {
+ ['groupLimit'] = 9,
+ },
+ ['AWACS'] = {
+ ['Overlord'] = 1,
+ ['Magic'] = 2,
+ ['Wizard'] = 3,
+ ['Focus'] = 4,
+ ['Darkstar'] = 5,
+ },
+ ['TANKER'] = {
+ ['Texaco'] = 1,
+ ['Arco'] = 2,
+ ['Shell'] = 3,
+ },
+ ['JTAC'] = {
+ ['Axeman'] = 1,
+ ['Darknight'] = 2,
+ ['Warrior'] = 3,
+ ['Pointer'] = 4,
+ ['Eyeball'] = 5,
+ ['Moonbeam'] = 6,
+ ['Whiplash'] = 7,
+ ['Finger'] = 8,
+ ['Pinpoint'] = 9,
+ ['Ferret'] = 10,
+ ['Shaba'] = 11,
+ ['Playboy'] = 12,
+ ['Hammer'] = 13,
+ ['Jaguar'] = 14,
+ ['Deathstar'] = 15,
+ ['Anvil'] = 16,
+ ['Firefly'] = 17,
+ ['Mantis'] = 18,
+ ['Badger'] = 19,
+ },
+ ['aircraft'] = {
+ ['Enfield'] = 1,
+ ['Springfield'] = 2,
+ ['Uzi'] = 3,
+ ['Colt'] = 4,
+ ['Dodge'] = 5,
+ ['Ford'] = 6,
+ ['Chevy'] = 7,
+ ['Pontiac'] = 8,
+ },
+
+ ['unique'] = {
+ ['A10'] = {
+ ['Hawg'] = 9,
+ ['Boar'] = 10,
+ ['Pig'] = 11,
+ ['Tusk'] = 12,
+ ['rules'] = {
+ ['canUseAircraft'] = true,
+ ['appliesTo'] = {
+ 'A-10C',
+ 'A-10A',
+ },
+ },
+ },
+ },
+ },
+ }
+ mist.DBs.const.shapeNames = {
+ ["Landmine"] = "landmine",
+ ["FARP CP Blindage"] = "kp_ug",
+ ["Subsidiary structure C"] = "saray-c",
+ ["Barracks 2"] = "kazarma2",
+ ["Small house 2C"] = "dom2c",
+ ["Military staff"] = "aviashtab",
+ ["Tech hangar A"] = "ceh_ang_a",
+ ["Oil derrick"] = "neftevyshka",
+ ["Tech combine"] = "kombinat",
+ ["Garage B"] = "garage_b",
+ ["Airshow_Crowd"] = "Crowd1",
+ ["Hangar A"] = "angar_a",
+ ["Repair workshop"] = "tech",
+ ["Subsidiary structure D"] = "saray-d",
+ ["FARP Ammo Dump Coating"] = "SetkaKP",
+ ["Small house 1C area"] = "dom2c-all",
+ ["Tank 2"] = "airbase_tbilisi_tank_01",
+ ["Boiler-house A"] = "kotelnaya_a",
+ ["Workshop A"] = "tec_a",
+ ["Small werehouse 1"] = "s1",
+ ["Garage small B"] = "garagh-small-b",
+ ["Small werehouse 4"] = "s4",
+ ["Shop"] = "magazin",
+ ["Subsidiary structure B"] = "saray-b",
+ ["FARP Fuel Depot"] = "GSM Rus",
+ ["Coach cargo"] = "wagon-gruz",
+ ["Electric power box"] = "tr_budka",
+ ["Tank 3"] = "airbase_tbilisi_tank_02",
+ ["Red_Flag"] = "H-flag_R",
+ ["Container red 3"] = "konteiner_red3",
+ ["Garage A"] = "garage_a",
+ ["Hangar B"] = "angar_b",
+ ["Black_Tyre"] = "H-tyre_B",
+ ["Cafe"] = "stolovaya",
+ ["Restaurant 1"] = "restoran1",
+ ["Subsidiary structure A"] = "saray-a",
+ ["Container white"] = "konteiner_white",
+ ["Warehouse"] = "sklad",
+ ["Tank"] = "bak",
+ ["Railway crossing B"] = "pereezd_small",
+ ["Subsidiary structure F"] = "saray-f",
+ ["Farm A"] = "ferma_a",
+ ["Small werehouse 3"] = "s3",
+ ["Water tower A"] = "wodokachka_a",
+ ["Railway station"] = "r_vok_sd",
+ ["Coach a tank blue"] = "wagon-cisterna_blue",
+ ["Supermarket A"] = "uniwersam_a",
+ ["Coach a platform"] = "wagon-platforma",
+ ["Garage small A"] = "garagh-small-a",
+ ["TV tower"] = "tele_bash",
+ ["Comms tower M"] = "tele_bash_m",
+ ["Small house 1A"] = "domik1a",
+ ["Farm B"] = "ferma_b",
+ ["GeneratorF"] = "GeneratorF",
+ ["Cargo1"] = "ab-212_cargo",
+ ["Container red 2"] = "konteiner_red2",
+ ["Subsidiary structure E"] = "saray-e",
+ ["Coach a passenger"] = "wagon-pass",
+ ["Black_Tyre_WF"] = "H-tyre_B_WF",
+ ["Electric locomotive"] = "elektrowoz",
+ ["Shelter"] = "ukrytie",
+ ["Coach a tank yellow"] = "wagon-cisterna_yellow",
+ ["Railway crossing A"] = "pereezd_big",
+ [".Ammunition depot"] = "SkladC",
+ ["Small werehouse 2"] = "s2",
+ ["Windsock"] = "H-Windsock_RW",
+ ["Shelter B"] = "ukrytie_b",
+ ["Fuel tank"] = "toplivo-bak",
+ ["Locomotive"] = "teplowoz",
+ [".Command Center"] = "ComCenter",
+ ["Pump station"] = "nasos",
+ ["Black_Tyre_RF"] = "H-tyre_B_RF",
+ ["Coach cargo open"] = "wagon-gruz-otkr",
+ ["Subsidiary structure 3"] = "hozdomik3",
+ ["FARP Tent"] = "PalatkaB",
+ ["White_Tyre"] = "H-tyre_W",
+ ["Subsidiary structure G"] = "saray-g",
+ ["Container red 1"] = "konteiner_red1",
+ ["Small house 1B area"] = "domik1b-all",
+ ["Subsidiary structure 1"] = "hozdomik1",
+ ["Container brown"] = "konteiner_brown",
+ ["Small house 1B"] = "domik1b",
+ ["Subsidiary structure 2"] = "hozdomik2",
+ ["Chemical tank A"] = "him_bak_a",
+ ["WC"] = "WC",
+ ["Small house 1A area"] = "domik1a-all",
+ ["White_Flag"] = "H-Flag_W",
+ ["Airshow_Cone"] = "Comp_cone",
+ }
+
+
+ -- create mist.DBs.oldAliveUnits
+ -- do
+ -- local intermediate_alive_units = {} -- between 0 and 0.5 secs old
+ -- local function make_old_alive_units() -- called every 0.5 secs, makes the old_alive_units DB which is just a copy of alive_units that is 0.5 to 1 sec old
+ -- if intermediate_alive_units then
+ -- mist.DBs.oldAliveUnits = mist.utils.deepCopy(intermediate_alive_units)
+ -- end
+ -- intermediate_alive_units = mist.utils.deepCopy(mist.DBs.aliveUnits)
+ -- timer.scheduleFunction(make_old_alive_units, nil, timer.getTime() + 0.5)
+ -- end
+
+ -- make_old_alive_units()
+ -- end
+
+ --Build DBs
+ for coa_name, coa_data in pairs(mist.DBs.units) do
+ for cntry_name, cntry_data in pairs(coa_data) do
+ for category_name, category_data in pairs(cntry_data) do
+ if type(category_data) == 'table' then
+ for group_ind, group_data in pairs(category_data) do
+ if type(group_data) == 'table' and group_data.units and type(group_data.units) == 'table' and #group_data.units > 0 then -- OCD paradigm programming
+ mist.DBs.groupsByName[group_data.groupName] = mist.utils.deepCopy(group_data)
+ mist.DBs.groupsById[group_data.groupId] = mist.utils.deepCopy(group_data)
+ for unit_ind, unit_data in pairs(group_data.units) do
+ mist.DBs.unitsByName[unit_data.unitName] = mist.utils.deepCopy(unit_data)
+ mist.DBs.unitsById[unit_data.unitId] = mist.utils.deepCopy(unit_data)
+
+ mist.DBs.unitsByCat[unit_data.category] = mist.DBs.unitsByCat[unit_data.category] or {} -- future-proofing against new categories...
+ table.insert(mist.DBs.unitsByCat[unit_data.category], mist.utils.deepCopy(unit_data))
+ dbLog:info('inserting $1', unit_data.unitName)
+ table.insert(mist.DBs.unitsByNum, mist.utils.deepCopy(unit_data))
+
+ if unit_data.skill and (unit_data.skill == "Client" or unit_data.skill == "Player") then
+ mist.DBs.humansByName[unit_data.unitName] = mist.utils.deepCopy(unit_data)
+ mist.DBs.humansById[unit_data.unitId] = mist.utils.deepCopy(unit_data)
+ --if Unit.getByName(unit_data.unitName) then
+ -- mist.DBs.activeHumans[unit_data.unitName] = mist.utils.deepCopy(unit_data)
+ -- mist.DBs.activeHumans[unit_data.unitName].playerName = Unit.getByName(unit_data.unitName):getPlayerName()
+ --end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ --DynDBs
+ mist.DBs.MEunits = mist.utils.deepCopy(mist.DBs.units)
+ mist.DBs.MEunitsByName = mist.utils.deepCopy(mist.DBs.unitsByName)
+ mist.DBs.MEunitsById = mist.utils.deepCopy(mist.DBs.unitsById)
+ mist.DBs.MEunitsByCat = mist.utils.deepCopy(mist.DBs.unitsByCat)
+ mist.DBs.MEunitsByNum = mist.utils.deepCopy(mist.DBs.unitsByNum)
+ mist.DBs.MEgroupsByName = mist.utils.deepCopy(mist.DBs.groupsByName)
+ mist.DBs.MEgroupsById = mist.utils.deepCopy(mist.DBs.groupsById)
+
+ mist.DBs.deadObjects = {}
+
+ do
+ local mt = {}
+
+ function mt.__newindex(t, key, val)
+ local original_key = key --only for duplicate runtime IDs.
+ local key_ind = 1
+ while mist.DBs.deadObjects[key] do
+ dbLog:warn('duplicate runtime id of previously dead object key: $1', key)
+ key = tostring(original_key) .. ' #' .. tostring(key_ind)
+ key_ind = key_ind + 1
+ end
+
+ if mist.DBs.aliveUnits and mist.DBs.aliveUnits[val.object.id_] then
+ --dbLog:info('object found in alive_units')
+ val.objectData = mist.utils.deepCopy(mist.DBs.aliveUnits[val.object.id_])
+ local pos = Object.getPosition(val.object)
+ if pos then
+ val.objectPos = pos.p
+ end
+ val.objectType = mist.DBs.aliveUnits[val.object.id_].category
+
+ elseif mist.DBs.removedAliveUnits and mist.DBs.removedAliveUnits[val.object.id_] then -- it didn't exist in alive_units, check old_alive_units
+ --dbLog:info('object found in old_alive_units')
+ val.objectData = mist.utils.deepCopy(mist.DBs.removedAliveUnits[val.object.id_])
+ local pos = Object.getPosition(val.object)
+ if pos then
+ val.objectPos = pos.p
+ end
+ val.objectType = mist.DBs.removedAliveUnits[val.object.id_].category
+
+ else --attempt to determine if static object...
+ --dbLog:info('object not found in alive units or old alive units')
+ local pos = Object.getPosition(val.object)
+ if pos then
+ local static_found = false
+ for ind, static in pairs(mist.DBs.unitsByCat.static) do
+ if ((pos.p.x - static.point.x)^2 + (pos.p.z - static.point.y)^2)^0.5 < 0.1 then --really, it should be zero...
+ dbLog:info('correlated dead static object to position')
+ val.objectData = static
+ val.objectPos = pos.p
+ val.objectType = 'static'
+ static_found = true
+ break
+ end
+ end
+ if not static_found then
+ val.objectPos = pos.p
+ val.objectType = 'building'
+ end
+ else
+ val.objectType = 'unknown'
+ end
+ end
+ rawset(t, key, val)
+ end
+
+ setmetatable(mist.DBs.deadObjects, mt)
+ end
+
+ do -- mist unitID funcs
+ for id, idData in pairs(mist.DBs.unitsById) do
+ if idData.unitId > mist.nextUnitId then
+ mist.nextUnitId = mist.utils.deepCopy(idData.unitId)
+ end
+ if idData.groupId > mist.nextGroupId then
+ mist.nextGroupId = mist.utils.deepCopy(idData.groupId)
+ end
+ end
+ end
+
+
+ end
+
+ local function updateAliveUnits() -- coroutine function
+ local lalive_units = mist.DBs.aliveUnits -- local references for faster execution
+ local lunits = mist.DBs.unitsByNum
+ local ldeepcopy = mist.utils.deepCopy
+ local lUnit = Unit
+ local lremovedAliveUnits = mist.DBs.removedAliveUnits
+ local updatedUnits = {}
+
+ if #lunits > 0 then
+ local units_per_run = math.ceil(#lunits/20)
+ if units_per_run < 5 then
+ units_per_run = 5
+ end
+
+ for i = 1, #lunits do
+ if lunits[i].category ~= 'static' then -- can't get statics with Unit.getByName :(
+ local unit = lUnit.getByName(lunits[i].unitName)
+ if unit then
+ --dbLog:info("unit named $1 alive!", lunits[i].unitName) -- spammy
+ local pos = unit:getPosition()
+ local newtbl = ldeepcopy(lunits[i])
+ if pos then
+ newtbl.pos = pos.p
+ end
+ newtbl.unit = unit
+ --newtbl.rt_id = unit.id_
+ lalive_units[unit.id_] = newtbl
+ updatedUnits[unit.id_] = true
+ end
+ end
+ if i%units_per_run == 0 then
+ coroutine.yield()
+ end
+ end
+ -- All units updated, remove any "alive" units that were not updated- they are dead!
+ for unit_id, unit in pairs(lalive_units) do
+ if not updatedUnits[unit_id] then
+ lremovedAliveUnits[unit_id] = unit
+ lalive_units[unit_id] = nil
+ end
+ end
+ end
+ end
+
+ local function dbUpdate(event, objType)
+ dbLog:info('dbUpdate')
+ local newTable = {}
+ newTable.startTime = 0
+ if type(event) == 'string' then -- if name of an object.
+ local newObject
+ if Group.getByName(event) then
+ newObject = Group.getByName(event)
+ elseif StaticObject.getByName(event) then
+ newObject = StaticObject.getByName(event)
+ -- log:info('its static')
+ else
+ log:warn('$1 is not a Unit or Static Object. This should not be possible', event)
+ return false
+ end
+
+ newTable.name = newObject:getName()
+ newTable.groupId = tonumber(newObject:getID())
+ newTable.groupName = newObject:getName()
+ local unitOneRef
+ if objType == 'static' then
+ unitOneRef = newObject
+ newTable.countryId = tonumber(newObject:getCountry())
+ newTable.coalitionId = tonumber(newObject:getCoalition())
+ newTable.category = 'static'
+ else
+ unitOneRef = newObject:getUnits()
+ newTable.countryId = tonumber(unitOneRef[1]:getCountry())
+ newTable.coalitionId = tonumber(unitOneRef[1]:getCoalition())
+ newTable.category = tonumber(newObject:getCategory())
+ end
+ for countryData, countryId in pairs(country.id) do
+ if newTable.country and string.upper(countryData) == string.upper(newTable.country) or countryId == newTable.countryId then
+ newTable.countryId = countryId
+ newTable.country = string.lower(countryData)
+ for coaData, coaId in pairs(coalition.side) do
+ if coaId == coalition.getCountryCoalition(countryId) then
+ newTable.coalition = string.lower(coaData)
+ end
+ end
+ end
+ end
+ for catData, catId in pairs(Unit.Category) do
+ if objType == 'group' and Group.getByName(newTable.groupName):isExist() then
+ if catId == Group.getByName(newTable.groupName):getCategory() then
+ newTable.category = string.lower(catData)
+ end
+ elseif objType == 'static' and StaticObject.getByName(newTable.groupName):isExist() then
+ if catId == StaticObject.getByName(newTable.groupName):getCategory() then
+ newTable.category = string.lower(catData)
+ end
+
+ end
+ end
+ local gfound = false
+ for index, data in pairs(mistAddedGroups) do
+ if mist.stringMatch(data.name, newTable.groupName) == true then
+ gfound = true
+ newTable.task = data.task
+ newTable.modulation = data.modulation
+ newTable.uncontrolled = data.uncontrolled
+ newTable.radioSet = data.radioSet
+ newTable.hidden = data.hidden
+ newTable.startTime = data.start_time
+ mistAddedGroups[index] = nil
+ end
+ end
+
+ if gfound == false then
+ newTable.uncontrolled = false
+ newTable.hidden = false
+ end
+
+ newTable.units = {}
+ if objType == 'group' then
+ for unitId, unitData in pairs(unitOneRef) do
+ newTable.units[unitId] = {}
+ newTable.units[unitId].unitName = unitData:getName()
+
+ newTable.units[unitId].x = mist.utils.round(unitData:getPosition().p.x)
+ newTable.units[unitId].y = mist.utils.round(unitData:getPosition().p.z)
+ newTable.units[unitId].point = {}
+ newTable.units[unitId].point.x = newTable.units[unitId].x
+ newTable.units[unitId].point.y = newTable.units[unitId].y
+ newTable.units[unitId].alt = mist.utils.round(unitData:getPosition().p.y)
+ newTable.units[unitId].speed = mist.vec.mag(unitData:getVelocity())
+
+ newTable.units[unitId].heading = mist.getHeading(unitData, true)
+
+ newTable.units[unitId].type = unitData:getTypeName()
+ newTable.units[unitId].unitId = tonumber(unitData:getID())
+
+
+ newTable.units[unitId].groupName = newTable.groupName
+ newTable.units[unitId].groupId = newTable.groupId
+ newTable.units[unitId].countryId = newTable.countryId
+ newTable.units[unitId].coalitionId = newTable.coalitionId
+ newTable.units[unitId].coalition = newTable.coalition
+ newTable.units[unitId].country = newTable.country
+ local found = false
+ for index, data in pairs(mistAddedObjects) do
+ if mist.stringMatch(data.name, newTable.units[unitId].unitName) == true then
+ found = true
+ newTable.units[unitId].livery_id = data.livery_id
+ newTable.units[unitId].skill = data.skill
+ newTable.units[unitId].alt_type = data.alt_type
+ newTable.units[unitId].callsign = data.callsign
+ newTable.units[unitId].psi = data.psi
+ mistAddedObjects[index] = nil
+ end
+ if found == false then
+ newTable.units[unitId].skill = "High"
+ newTable.units[unitId].alt_type = "BARO"
+ end
+ end
+
+ end
+ else -- its a static
+ newTable.category = 'static'
+ newTable.units[1] = {}
+ newTable.units[1].unitName = newObject:getName()
+ newTable.units[1].category = 'static'
+ newTable.units[1].x = mist.utils.round(newObject:getPosition().p.x)
+ newTable.units[1].y = mist.utils.round(newObject:getPosition().p.z)
+ newTable.units[1].point = {}
+ newTable.units[1].point.x = newTable.units[1].x
+ newTable.units[1].point.y = newTable.units[1].y
+ newTable.units[1].alt = mist.utils.round(newObject:getPosition().p.y)
+ newTable.units[1].heading = mist.getHeading(newObject, true)
+ newTable.units[1].type = newObject:getTypeName()
+ newTable.units[1].unitId = tonumber(newObject:getID())
+ newTable.units[1].groupName = newTable.name
+ newTable.units[1].groupId = newTable.groupId
+ newTable.units[1].countryId = newTable.countryId
+ newTable.units[1].country = newTable.country
+ newTable.units[1].coalitionId = newTable.coalitionId
+ newTable.units[1].coalition = newTable.coalition
+ if newObject:getCategory() == 6 and newObject:getCargoDisplayName() then
+ local mass = newObject:getCargoDisplayName()
+ mass = string.gsub(mass, ' ', '')
+ mass = string.gsub(mass, 'kg', '')
+ newTable.units[1].mass = tonumber(mass)
+ newTable.units[1].categoryStatic = 'Cargos'
+ newTable.units[1].canCargo = true
+ newTable.units[1].shape_name = 'ab-212_cargo'
+ end
+
+ ----- search mist added objects for extra data if applicable
+ for index, data in pairs(mistAddedObjects) do
+ if mist.stringMatch(data.name, newTable.units[1].unitName) == true then
+ newTable.units[1].shape_name = data.shape_name -- for statics
+ newTable.units[1].livery_id = data.livery_id
+ newTable.units[1].airdromeId = data.airdromeId
+ newTable.units[1].mass = data.mass
+ newTable.units[1].canCargo = data.canCargo
+ newTable.units[1].categoryStatic = data.categoryStatic
+ newTable.units[1].type = 'cargo1'
+ mistAddedObjects[index] = nil
+ end
+ end
+ end
+ end
+ --mist.debug.writeData(mist.utils.serialize,{'msg', newTable}, timer.getAbsTime() ..'Group.lua')
+ newTable.timeAdded = timer.getAbsTime() -- only on the dynGroupsAdded table. For other reference, see start time
+ --mist.debug.dumpDBs()
+ --end
+ dbLog:info('endDbUpdate')
+ return newTable
+ end
+
+ --[[DB update code... FRACK. I need to refactor some of it.
+
+ The problem is that the DBs need to account better for shared object names. Needs to write over some data and outright remove other.
+
+ If groupName is used then entire group needs to be rewritten
+ what to do with old groups units DB entries?. Names cant be assumed to be the same.
+
+
+ -- new spawn event check.
+ -- event handler filters everything into groups: tempSpawnedGroups
+ -- this function then checks DBs to see if data has changed
+ ]]
+ local function checkSpawnedEventsNew()
+ if tempSpawnGroupsCounter > 0 then
+ --[[local updatesPerRun = math.ceil(#tempSpawnedGroupsCounter/20)
+ if updatesPerRun < 5 then
+ updatesPerRun = 5
+ end]]
+
+ dbLog:info('iterate')
+ for name, gType in pairs(tempSpawnedGroups) do
+ dbLog:info(name)
+ local updated = false
+
+ if mist.DBs.groupsByName[name] then
+ -- first check group level properties, groupId, countryId, coalition
+ dbLog:info('Found in DBs, check if updated')
+ local dbTable = mist.DBs.groupsByName[name]
+ dbLog:info(dbTable)
+ if gType ~= 'static' then
+ dbLog:info('Not static')
+ local _g = Group.getByName(name)
+ local _u = _g:getUnit(1)
+ if dbTable.groupId ~= tonumber(_g:getID()) or _u:getCountry() ~= dbTable.countryId or _u:getCoalition() ~= dbTable.coaltionId then
+ dbLog:info('Group Data mismatch')
+ updated = true
+ else
+ dbLog:info('No Mismatch')
+ end
+
+ end
+ end
+ dbLog:info('Updated: $1', updated)
+ if updated == false and gType ~= 'static' then -- time to check units
+ dbLog:info('No Group Mismatch, Check Units')
+ for index, uObject in pairs(Group.getByName(name):getUnits()) do
+ dbLog:info(index)
+ if mist.DBs.unitsByName[uObject:getName()] then
+ dbLog:info('UnitByName table exists')
+ local uTable = mist.DBs.unitsByName[uObject:getName()]
+ if tonumber(uObject:getID()) ~= uTable.unitId or uObject:getTypeName() ~= uTable.type then
+ dbLog:info('Unit Data mismatch')
+ updated = true
+ break
+ end
+ end
+ end
+ end
+
+ if updated == true or not mist.DBs.groupsByName[name] then
+ dbLog:info('Get Table')
+ writeGroups[#writeGroups+1] = {data = dbUpdate(name, gType), isUpdated = updated}
+
+ end
+ -- Work done, so remove
+ tempSpawnedGroups[name] = nil
+ tempSpawnGroupsCounter = tempSpawnGroupsCounter - 1
+ end
+ end
+ end
+
+ local function updateDBTables()
+ local i = #writeGroups
+
+ local savesPerRun = math.ceil(i/10)
+ if savesPerRun < 5 then
+ savesPerRun = 5
+ end
+ if i > 0 then
+ dbLog:info('updateDBTables')
+ local ldeepCopy = mist.utils.deepCopy
+ for x = 1, i do
+ dbLog:info(writeGroups[x])
+ local newTable = writeGroups[x].data
+ local updated = writeGroups[x].isUpdated
+ local mistCategory
+ if type(newTable.category) == 'string' then
+ mistCategory = string.lower(newTable.category)
+ end
+
+ if string.upper(newTable.category) == 'GROUND_UNIT' then
+ mistCategory = 'vehicle'
+ newTable.category = mistCategory
+ elseif string.upper(newTable.category) == 'AIRPLANE' then
+ mistCategory = 'plane'
+ newTable.category = mistCategory
+ elseif string.upper(newTable.category) == 'HELICOPTER' then
+ mistCategory = 'helicopter'
+ newTable.category = mistCategory
+ elseif string.upper(newTable.category) == 'SHIP' then
+ mistCategory = 'ship'
+ newTable.category = mistCategory
+ end
+ dbLog:info('Update unitsBy')
+ for newId, newUnitData in pairs(newTable.units) do
+ dbLog:info(newId)
+ newUnitData.category = mistCategory
+ if newUnitData.unitId then
+ dbLog:info('byId')
+ mist.DBs.unitsById[tonumber(newUnitData.unitId)] = ldeepCopy(newUnitData)
+ end
+ dbLog:info(updated)
+ if mist.DBs.unitsByName[newUnitData.unitName] and updated == true then--if unit existed before and something was updated, write over the entry for a given unit name just in case.
+ dbLog:info('Updating Unit Tables')
+ for i = 1, #mist.DBs.unitsByCat[mistCategory] do
+ if mist.DBs.unitsByCat[mistCategory][i].unitName == newUnitData.unitName then
+ dbLog:info('Entry Found, Rewriting for unitsByCat')
+ mist.DBs.unitsByCat[mistCategory][i] = ldeepCopy(newUnitData)
+ break
+ end
+ end
+ for i = 1, #mist.DBs.unitsByNum do
+ if mist.DBs.unitsByNum[i].unitName == newUnitData.unitName then
+ dbLog:info('Entry Found, Rewriting for unitsByNum')
+ mist.DBs.unitsByNum[i] = ldeepCopy(newUnitData)
+ break
+ end
+ end
+
+ else
+ dbLog:info('Unitname not in use, add as normal')
+ mist.DBs.unitsByCat[mistCategory][#mist.DBs.unitsByCat[mistCategory] + 1] = ldeepCopy(newUnitData)
+ mist.DBs.unitsByNum[#mist.DBs.unitsByNum + 1] = ldeepCopy(newUnitData)
+ end
+ mist.DBs.unitsByName[newUnitData.unitName] = ldeepCopy(newUnitData)
+
+
+ end
+ -- this is a really annoying DB to populate. Gotta create new tables in case its missing
+ dbLog:info('write mist.DBs.units')
+ if not mist.DBs.units[newTable.coalition] then
+ mist.DBs.units[newTable.coalition] = {}
+ end
+
+ if not mist.DBs.units[newTable.coalition][newTable.country] then
+ mist.DBs.units[newTable.coalition][(newTable.country)] = {}
+ mist.DBs.units[newTable.coalition][(newTable.country)].countryId = newTable.countryId
+ end
+ if not mist.DBs.units[newTable.coalition][newTable.country][mistCategory] then
+ mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory] = {}
+ end
+
+ if updated == true then
+ dbLog:info('Updating DBsUnits')
+ for i = 1, #mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory] do
+ if mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory][i].groupName == newTable.groupName then
+ dbLog:info('Entry Found, Rewriting')
+ mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory][i] = ldeepCopy(newTable)
+ break
+ end
+ end
+ else
+ mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory][#mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory] + 1] = ldeepCopy(newTable)
+ end
+
+
+ if newTable.groupId then
+ mist.DBs.groupsById[newTable.groupId] = ldeepCopy(newTable)
+ end
+
+ mist.DBs.groupsByName[newTable.name] = ldeepCopy(newTable)
+ mist.DBs.dynGroupsAdded[#mist.DBs.dynGroupsAdded + 1] = ldeepCopy(newTable)
+
+ writeGroups[x] = nil
+ if x%savesPerRun == 0 then
+ coroutine.yield()
+ end
+ end
+ if timer.getTime() > lastUpdateTime then
+ lastUpdateTime = timer.getTime()
+ end
+ dbLog:info('endUpdateTables')
+ end
+ end
+
+ local function groupSpawned(event)
+ -- dont need to add units spawned in at the start of the mission if mist is loaded in init line
+ if event.id == world.event.S_EVENT_BIRTH and timer.getTime0() < timer.getAbsTime() then
+ dbLog:info('unitSpawnEvent')
+
+ --table.insert(tempSpawnedUnits,(event.initiator))
+ -------
+ -- New functionality below.
+ -------
+ if Object.getCategory(event.initiator) == 1 and not Unit.getPlayerName(event.initiator) then -- simple player check, will need to later check to see if unit was spawned with a player in a flight
+ dbLog:info('Object is a Unit')
+ dbLog:info(Unit.getGroup(event.initiator):getName())
+ if not tempSpawnedGroups[Unit.getGroup(event.initiator):getName()] then
+ dbLog:info('added')
+ tempSpawnedGroups[Unit.getGroup(event.initiator):getName()] = 'group'
+ tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1
+ end
+ elseif Object.getCategory(event.initiator) == 3 or Object.getCategory(event.initiator) == 6 then
+ dbLog:info('Object is Static')
+ tempSpawnedGroups[StaticObject.getName(event.initiator)] = 'static'
+ tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1
+ end
+
+
+ end
+ end
+
+ local function doScheduledFunctions()
+ local i = 1
+ while i <= #scheduledTasks do
+ if not scheduledTasks[i].rep then -- not a repeated process
+ if scheduledTasks[i].t <= timer.getTime() then
+ local task = scheduledTasks[i] -- local reference
+ table.remove(scheduledTasks, i)
+ local err, errmsg = pcall(task.f, unpack(task.vars, 1, table.maxn(task.vars)))
+ if not err then
+ log:error('Error in scheduled function: $1', errmsg)
+ end
+ --task.f(unpack(task.vars, 1, table.maxn(task.vars))) -- do the task, do not increment i
+ else
+ i = i + 1
+ end
+ else
+ if scheduledTasks[i].st and scheduledTasks[i].st <= timer.getTime() then --if a stoptime was specified, and the stop time exceeded
+ table.remove(scheduledTasks, i) -- stop time exceeded, do not execute, do not increment i
+ elseif scheduledTasks[i].t <= timer.getTime() then
+ local task = scheduledTasks[i] -- local reference
+ task.t = timer.getTime() + task.rep --schedule next run
+ local err, errmsg = pcall(task.f, unpack(task.vars, 1, table.maxn(task.vars)))
+ if not err then
+ log:error('Error in scheduled function: $1' .. errmsg)
+ end
+ --scheduledTasks[i].f(unpack(scheduledTasks[i].vars, 1, table.maxn(scheduledTasks[i].vars))) -- do the task
+ i = i + 1
+ else
+ i = i + 1
+ end
+ end
+ end
+ end
+
+ -- Event handler to start creating the dead_objects table
+ local function addDeadObject(event)
+ if event.id == world.event.S_EVENT_DEAD or event.id == world.event.S_EVENT_CRASH then
+ if event.initiator and event.initiator.id_ and event.initiator.id_ > 0 then
+
+ local id = event.initiator.id_ -- initial ID, could change if there is a duplicate id_ already dead.
+ local val = {object = event.initiator} -- the new entry in mist.DBs.deadObjects.
+
+ local original_id = id --only for duplicate runtime IDs.
+ local id_ind = 1
+ while mist.DBs.deadObjects[id] do
+ --log:info('duplicate runtime id of previously dead object id: $1', id)
+ id = tostring(original_id) .. ' #' .. tostring(id_ind)
+ id_ind = id_ind + 1
+ end
+
+ if mist.DBs.aliveUnits and mist.DBs.aliveUnits[val.object.id_] then
+ --log:info('object found in alive_units')
+ val.objectData = mist.utils.deepCopy(mist.DBs.aliveUnits[val.object.id_])
+ local pos = Object.getPosition(val.object)
+ if pos then
+ val.objectPos = pos.p
+ end
+ val.objectType = mist.DBs.aliveUnits[val.object.id_].category
+ --[[if mist.DBs.activeHumans[Unit.getName(val.object)] then
+ --trigger.action.outText('remove via death: ' .. Unit.getName(val.object),20)
+ mist.DBs.activeHumans[Unit.getName(val.object)] = nil
+ end]]
+ elseif mist.DBs.removedAliveUnits and mist.DBs.removedAliveUnits[val.object.id_] then -- it didn't exist in alive_units, check old_alive_units
+ --log:info('object found in old_alive_units')
+ val.objectData = mist.utils.deepCopy(mist.DBs.removedAliveUnits[val.object.id_])
+ local pos = Object.getPosition(val.object)
+ if pos then
+ val.objectPos = pos.p
+ end
+ val.objectType = mist.DBs.removedAliveUnits[val.object.id_].category
+
+ else --attempt to determine if static object...
+ --log:info('object not found in alive units or old alive units')
+ local pos = Object.getPosition(val.object)
+ if pos then
+ local static_found = false
+ for ind, static in pairs(mist.DBs.unitsByCat.static) do
+ if ((pos.p.x - static.point.x)^2 + (pos.p.z - static.point.y)^2)^0.5 < 0.1 then --really, it should be zero...
+ --log:info('correlated dead static object to position')
+ val.objectData = static
+ val.objectPos = pos.p
+ val.objectType = 'static'
+ static_found = true
+ break
+ end
+ end
+ if not static_found then
+ val.objectPos = pos.p
+ val.objectType = 'building'
+ end
+ else
+ val.objectType = 'unknown'
+ end
+ end
+ mist.DBs.deadObjects[id] = val
+ end
+ end
+ end
+
+ --[[
+ local function addClientsToActive(event)
+ if event.id == world.event.S_EVENT_PLAYER_ENTER_UNIT or event.id == world.event.S_EVENT_BIRTH then
+ log:info(event)
+ if Unit.getPlayerName(event.initiator) then
+ log:info(Unit.getPlayerName(event.initiator))
+ local newU = mist.utils.deepCopy(mist.DBs.unitsByName[Unit.getName(event.initiator)])
+ newU.playerName = Unit.getPlayerName(event.initiator)
+ mist.DBs.activeHumans[Unit.getName(event.initiator)] = newU
+ --trigger.action.outText('added: ' .. Unit.getName(event.initiator), 20)
+ end
+ elseif event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT and event.initiator then
+ if mist.DBs.activeHumans[Unit.getName(event.initiator)] then
+ mist.DBs.activeHumans[Unit.getName(event.initiator)] = nil
+ -- trigger.action.outText('removed via control: ' .. Unit.getName(event.initiator), 20)
+ end
+ end
+ end
+
+ mist.addEventHandler(addClientsToActive)
+ ]]
+
+ --- init function.
+ -- creates logger, adds default event handler
+ -- and calls main the first time.
+ -- @function mist.init
+ function mist.init()
+ -- create logger
+ mist.log = mist.Logger:new("MIST")
+ dbLog = mist.Logger:new('MISTDB', 'warning')
+
+ log = mist.log -- log shorthand
+ -- set warning log level, showing only
+ -- warnings and errors
+ log:setLevel("warning")
+
+ log:info("initializing databases")
+ initDBs()
+
+ -- add event handler for group spawns
+ mist.addEventHandler(groupSpawned)
+ mist.addEventHandler(addDeadObject)
+
+ -- call main the first time therafter it reschedules itself.
+ mist.main()
+ --log:msg('MIST version $1.$2.$3 loaded', mist.majorVersion, mist.minorVersion, mist.build)
+ return
+ end
+
+ --- The main function.
+ -- Run 100 times per second.
+ -- You shouldn't call this function.
+ function mist.main()
+ timer.scheduleFunction(mist.main, {}, timer.getTime() + 0.01) --reschedule first in case of Lua error
+
+ updateTenthSecond = updateTenthSecond + 1
+ if updateTenthSecond == 10 then
+ updateTenthSecond = 0
+
+ checkSpawnedEventsNew()
+
+ if not coroutines.updateDBTables then
+ coroutines.updateDBTables = coroutine.create(updateDBTables)
+ end
+
+ coroutine.resume(coroutines.updateDBTables)
+
+ if coroutine.status(coroutines.updateDBTables) == 'dead' then
+ coroutines.updateDBTables = nil
+ end
+ end
+
+ --updating alive units
+ updateAliveUnitsCounter = updateAliveUnitsCounter + 1
+ if updateAliveUnitsCounter == 5 then
+ updateAliveUnitsCounter = 0
+
+ if not coroutines.updateAliveUnits then
+ coroutines.updateAliveUnits = coroutine.create(updateAliveUnits)
+ end
+
+ coroutine.resume(coroutines.updateAliveUnits)
+
+ if coroutine.status(coroutines.updateAliveUnits) == 'dead' then
+ coroutines.updateAliveUnits = nil
+ end
+ end
+
+ doScheduledFunctions()
+ end -- end of mist.main
+
+ --- Returns next unit id.
+ -- @treturn number next unit id.
+ function mist.getNextUnitId()
+ mist.nextUnitId = mist.nextUnitId + 1
+ if mist.nextUnitId > 6900 then
+ mist.nextUnitId = 14000
+ end
+ return mist.nextUnitId
+ end
+
+ --- Returns next group id.
+ -- @treturn number next group id.
+ function mist.getNextGroupId()
+ mist.nextGroupId = mist.nextGroupId + 1
+ if mist.nextGroupId > 6900 then
+ mist.nextGroupId = 14000
+ end
+ return mist.nextGroupId
+ end
+
+ --- Returns timestamp of last database update.
+ -- @treturn timestamp of last database update
+ function mist.getLastDBUpdateTime()
+ return lastUpdateTime
+ end
+
+ --- Spawns a static object to the game world.
+ -- @todo write good docs
+ -- @tparam table staticObj table containing data needed for the object creation
+ function mist.dynAddStatic(newObj)
+
+ if newObj.units and newObj.units[1] then -- if its mist format
+ for entry, val in pairs(newObj.units[1]) do
+ if newObj[entry] and newObj[entry] ~= val or not newObj[entry] then
+ newObj[entry] = val
+ end
+ end
+ end
+ --log:info(newObj)
+
+ local cntry = newObj.country
+ if newObj.countryId then
+ cntry = newObj.countryId
+ end
+
+ local newCountry = ''
+
+ for countryId, countryName in pairs(country.name) do
+ if type(cntry) == 'string' then
+ cntry = cntry:gsub("%s+", "_")
+ if tostring(countryName) == string.upper(cntry) then
+ newCountry = countryName
+ end
+ elseif type(cntry) == 'number' then
+ if countryId == cntry then
+ newCountry = countryName
+ end
+ end
+ end
+
+ if newCountry == '' then
+ log:error("Country not found: $1", cntry)
+ return false
+ end
+
+ if newObj.clone or not newObj.groupId then
+ mistGpId = mistGpId + 1
+ newObj.groupId = mistGpId
+ end
+
+ if newObj.clone or not newObj.unitId then
+ mistUnitId = mistUnitId + 1
+ newObj.unitId = mistUnitId
+ end
+
+ if newObj.clone or not newObj.name then
+ mistDynAddIndex[' static '] = mistDynAddIndex[' static '] + 1
+ newObj.name = (newCountry .. ' static ' .. mistDynAddIndex[' static '])
+ end
+
+ if not newObj.dead then
+ newObj.dead = false
+ end
+
+ if not newObj.heading then
+ newObj.heading = math.random(360)
+ end
+
+ if newObj.categoryStatic then
+ newObj.category = newObj.categoryStatic
+ end
+ if newObj.mass then
+ newObj.category = 'Cargos'
+ end
+
+ if newObj.shapeName then
+ newObj.shape_name = newObj.shapeName
+ end
+
+ if not newObj.shape_name then
+ log:info('shape_name not present')
+ if mist.DBs.const.shapeNames[newObj.type] then
+ newObj.shape_name = mist.DBs.const.shapeNames[newObj.type]
+ end
+ end
+
+ mistAddedObjects[#mistAddedObjects + 1] = mist.utils.deepCopy(newObj)
+ if newObj.x and newObj.y and newObj.type and type(newObj.x) == 'number' and type(newObj.y) == 'number' and type(newObj.type) == 'string' then
+ --log:info('addStaticObject')
+ coalition.addStaticObject(country.id[newCountry], newObj)
+
+ return newObj
+ end
+ log:error("Failed to add static object due to missing or incorrect value. X: $1, Y: $2, Type: $3", newObj.x, newObj.y, newObj.type)
+ return false
+ end
+
+ --- Spawns a dynamic group into the game world.
+ -- Same as coalition.add function in SSE. checks the passed data to see if its valid.
+ -- Will generate groupId, groupName, unitId, and unitName if needed
+ -- @tparam table newGroup table containting values needed for spawning a group.
+ function mist.dynAdd(newGroup)
+
+ --mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, 'newGroupOrig.lua')
+ local cntry = newGroup.country
+ if newGroup.countryId then
+ cntry = newGroup.countryId
+ end
+
+ local groupType = newGroup.category
+ local newCountry = ''
+ -- validate data
+ for countryId, countryName in pairs(country.name) do
+ if type(cntry) == 'string' then
+ cntry = cntry:gsub("%s+", "_")
+ if tostring(countryName) == string.upper(cntry) then
+ newCountry = countryName
+ end
+ elseif type(cntry) == 'number' then
+ if countryId == cntry then
+ newCountry = countryName
+ end
+ end
+ end
+
+ if newCountry == '' then
+ log:error("Country not found: $1", cntry)
+ return false
+ end
+
+ local newCat = ''
+ for catName, catId in pairs(Unit.Category) do
+ if type(groupType) == 'string' then
+ if tostring(catName) == string.upper(groupType) then
+ newCat = catName
+ end
+ elseif type(groupType) == 'number' then
+ if catId == groupType then
+ newCat = catName
+ end
+ end
+
+ if catName == 'GROUND_UNIT' and (string.upper(groupType) == 'VEHICLE' or string.upper(groupType) == 'GROUND') then
+ newCat = 'GROUND_UNIT'
+ elseif catName == 'AIRPLANE' and string.upper(groupType) == 'PLANE' then
+ newCat = 'AIRPLANE'
+ end
+ end
+ local typeName
+ if newCat == 'GROUND_UNIT' then
+ typeName = ' gnd '
+ elseif newCat == 'AIRPLANE' then
+ typeName = ' air '
+ elseif newCat == 'HELICOPTER' then
+ typeName = ' hel '
+ elseif newCat == 'SHIP' then
+ typeName = ' shp '
+ elseif newCat == 'BUILDING' then
+ typeName = ' bld '
+ end
+ if newGroup.clone or not newGroup.groupId then
+ mistDynAddIndex[typeName] = mistDynAddIndex[typeName] + 1
+ mistGpId = mistGpId + 1
+ newGroup.groupId = mistGpId
+ end
+ if newGroup.groupName or newGroup.name then
+ if newGroup.groupName then
+ newGroup.name = newGroup.groupName
+ elseif newGroup.name then
+ newGroup.name = newGroup.name
+ end
+ end
+
+ if newGroup.clone and mist.DBs.groupsByName[newGroup.name] or not newGroup.name then
+ newGroup.name = tostring(newCountry .. tostring(typeName) .. mistDynAddIndex[typeName])
+ end
+
+ if not newGroup.hidden then
+ newGroup.hidden = false
+ end
+
+ if not newGroup.visible then
+ newGroup.visible = false
+ end
+
+ if (newGroup.start_time and type(newGroup.start_time) ~= 'number') or not newGroup.start_time then
+ if newGroup.startTime then
+ newGroup.start_time = mist.utils.round(newGroup.startTime)
+ else
+ newGroup.start_time = 0
+ end
+ end
+
+
+ for unitIndex, unitData in pairs(newGroup.units) do
+ local originalName = newGroup.units[unitIndex].unitName or newGroup.units[unitIndex].name
+ if newGroup.clone or not unitData.unitId then
+ mistUnitId = mistUnitId + 1
+ newGroup.units[unitIndex].unitId = mistUnitId
+ end
+ if newGroup.units[unitIndex].unitName or newGroup.units[unitIndex].name then
+ if newGroup.units[unitIndex].unitName then
+ newGroup.units[unitIndex].name = newGroup.units[unitIndex].unitName
+ elseif newGroup.units[unitIndex].name then
+ newGroup.units[unitIndex].name = newGroup.units[unitIndex].name
+ end
+ end
+ if newGroup.clone or not unitData.name then
+ newGroup.units[unitIndex].name = tostring(newGroup.name .. ' unit' .. unitIndex)
+ end
+
+ if not unitData.skill then
+ newGroup.units[unitIndex].skill = 'Random'
+ end
+
+ if not unitData.alt then
+ if newCat == 'AIRPLANE' then
+ newGroup.units[unitIndex].alt = 2000
+ newGroup.units[unitIndex].alt_type = 'RADIO'
+ newGroup.units[unitIndex].speed = 150
+ elseif newCat == 'HELICOPTER' then
+ newGroup.units[unitIndex].alt = 500
+ newGroup.units[unitIndex].alt_type = 'RADIO'
+ newGroup.units[unitIndex].speed = 60
+ else
+ --[[log:info('check height')
+ newGroup.units[unitIndex].alt = land.getHeight({x = newGroup.units[unitIndex].x, y = newGroup.units[unitIndex].y})
+ newGroup.units[unitIndex].alt_type = 'BARO']]
+ end
+
+
+ end
+
+ if newCat == 'AIRPLANE' or newCat == 'HELICOPTER' then
+ if newGroup.units[unitIndex].alt_type and newGroup.units[unitIndex].alt_type ~= 'BARO' or not newGroup.units[unitIndex].alt_type then
+ newGroup.units[unitIndex].alt_type = 'RADIO'
+ end
+ if not unitData.speed then
+ if newCat == 'AIRPLANE' then
+ newGroup.units[unitIndex].speed = 150
+ elseif newCat == 'HELICOPTER' then
+ newGroup.units[unitIndex].speed = 60
+ end
+ end
+ if not unitData.payload then
+ newGroup.units[unitIndex].payload = mist.getPayload(originalName)
+ end
+ end
+ mistAddedObjects[#mistAddedObjects + 1] = mist.utils.deepCopy(newGroup.units[unitIndex])
+ end
+ mistAddedGroups[#mistAddedGroups + 1] = mist.utils.deepCopy(newGroup)
+ if newGroup.route and not newGroup.route.points then
+ if not newGroup.route.points and newGroup.route[1] then
+ local copyRoute = newGroup.route
+ newGroup.route = {}
+ newGroup.route.points = copyRoute
+ end
+ end
+ newGroup.country = newCountry
+
+
+ --mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, 'newGroup.lua')
+
+ -- sanitize table
+ newGroup.groupName = nil
+ newGroup.clone = nil
+ newGroup.category = nil
+ newGroup.country = nil
+
+ newGroup.tasks = {}
+
+ for unitIndex, unitData in pairs(newGroup.units) do
+ newGroup.units[unitIndex].unitName = nil
+ end
+
+ coalition.addGroup(country.id[newCountry], Unit.Category[newCat], newGroup)
+
+ return newGroup
+
+ end
+
+ --- Schedules a function.
+ -- Modified Slmod task scheduler, superior to timer.scheduleFunction
+ -- @tparam function f function to schedule
+ -- @tparam table vars array containing all parameters passed to the function
+ -- @tparam number t time in seconds from mission start to schedule the function to.
+ -- @tparam[opt] number rep time between repetitions of the function
+ -- @tparam[opt] number st time in seconds from mission start at which the function
+ -- should stop to be rescheduled.
+ -- @treturn number scheduled function id.
+ function mist.scheduleFunction(f, vars, t, rep, st)
+ --verify correct types
+ assert(type(f) == 'function', 'variable 1, expected function, got ' .. type(f))
+ assert(type(vars) == 'table' or vars == nil, 'variable 2, expected table or nil, got ' .. type(f))
+ assert(type(t) == 'number', 'variable 3, expected number, got ' .. type(t))
+ assert(type(rep) == 'number' or rep == nil, 'variable 4, expected number or nil, got ' .. type(rep))
+ assert(type(st) == 'number' or st == nil, 'variable 5, expected number or nil, got ' .. type(st))
+ if not vars then
+ vars = {}
+ end
+ taskId = taskId + 1
+ table.insert(scheduledTasks, {f = f, vars = vars, t = t, rep = rep, st = st, id = taskId})
+ return taskId
+ end
+
+ --- Removes a scheduled function.
+ -- @tparam number id function id
+ -- @treturn boolean true if function was successfully removed, false otherwise.
+ function mist.removeFunction(id)
+ local i = 1
+ while i <= #scheduledTasks do
+ if scheduledTasks[i].id == id then
+ table.remove(scheduledTasks, i)
+ else
+ i = i + 1
+ end
+ end
+ end
+
+ --- Registers an event handler.
+ -- @tparam function f function handling event
+ -- @treturn number id of the event handler
+ function mist.addEventHandler(f) --id is optional!
+ local handler = {}
+ idNum = idNum + 1
+ handler.id = idNum
+ handler.f = f
+ function handler:onEvent(event)
+ self.f(event)
+ end
+ world.addEventHandler(handler)
+ return handler.id
+ end
+
+ --- Removes event handler with given id.
+ -- @tparam number id event handler id
+ -- @treturn boolean true on success, false otherwise
+ function mist.removeEventHandler(id)
+ for key, handler in pairs(world.eventHandlers) do
+ if handler.id and handler.id == id then
+ world.eventHandlers[key] = nil
+ return true
+ end
+ end
+ return false
+ end
+end
+
+-- Begin common funcs
+do
+ --- Returns MGRS coordinates as string.
+ -- @tparam string MGRS MGRS coordinates
+ -- @tparam number acc the accuracy of each easting/northing.
+ -- Can be: 0, 1, 2, 3, 4, or 5.
+ function mist.tostringMGRS(MGRS, acc)
+ if acc == 0 then
+ return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph
+ else
+ return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', mist.utils.round(MGRS.Easting/(10^(5-acc)), 0))
+ .. ' ' .. string.format('%0' .. acc .. 'd', mist.utils.round(MGRS.Northing/(10^(5-acc)), 0))
+ end
+ end
+
+ --[[acc:
+ in DM: decimal point of minutes.
+ In DMS: decimal point of seconds.
+ position after the decimal of the least significant digit:
+ So:
+ 42.32 - acc of 2.
+ ]]
+ function mist.tostringLL(lat, lon, acc, DMS)
+
+ local latHemi, lonHemi
+ if lat > 0 then
+ latHemi = 'N'
+ else
+ latHemi = 'S'
+ end
+
+ if lon > 0 then
+ lonHemi = 'E'
+ else
+ lonHemi = 'W'
+ end
+
+ lat = math.abs(lat)
+ lon = math.abs(lon)
+
+ local latDeg = math.floor(lat)
+ local latMin = (lat - latDeg)*60
+
+ local lonDeg = math.floor(lon)
+ local lonMin = (lon - lonDeg)*60
+
+ if DMS then -- degrees, minutes, and seconds.
+ local oldLatMin = latMin
+ latMin = math.floor(latMin)
+ local latSec = mist.utils.round((oldLatMin - latMin)*60, acc)
+
+ local oldLonMin = lonMin
+ lonMin = math.floor(lonMin)
+ local lonSec = mist.utils.round((oldLonMin - lonMin)*60, acc)
+
+ if latSec == 60 then
+ latSec = 0
+ latMin = latMin + 1
+ end
+
+ if lonSec == 60 then
+ lonSec = 0
+ lonMin = lonMin + 1
+ end
+
+ local secFrmtStr -- create the formatting string for the seconds place
+ if acc <= 0 then -- no decimal place.
+ secFrmtStr = '%02d'
+ else
+ local width = 3 + acc -- 01.310 - that's a width of 6, for example.
+ secFrmtStr = '%0' .. width .. '.' .. acc .. 'f'
+ end
+
+ return string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' '
+ .. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi
+
+ else -- degrees, decimal minutes.
+ latMin = mist.utils.round(latMin, acc)
+ lonMin = mist.utils.round(lonMin, acc)
+
+ if latMin == 60 then
+ latMin = 0
+ latDeg = latDeg + 1
+ end
+
+ if lonMin == 60 then
+ lonMin = 0
+ lonDeg = lonDeg + 1
+ end
+
+ local minFrmtStr -- create the formatting string for the minutes place
+ if acc <= 0 then -- no decimal place.
+ minFrmtStr = '%02d'
+ else
+ local width = 3 + acc -- 01.310 - that's a width of 6, for example.
+ minFrmtStr = '%0' .. width .. '.' .. acc .. 'f'
+ end
+
+ return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' '
+ .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi
+
+ end
+ end
+
+ --[[ required: az - radian
+ required: dist - meters
+ optional: alt - meters (set to false or nil if you don't want to use it).
+ optional: metric - set true to get dist and alt in km and m.
+ precision will always be nearest degree and NM or km.]]
+ function mist.tostringBR(az, dist, alt, metric)
+ az = mist.utils.round(mist.utils.toDegree(az), 0)
+
+ if metric then
+ dist = mist.utils.round(dist/1000, 0)
+ else
+ dist = mist.utils.round(mist.utils.metersToNM(dist), 0)
+ end
+
+ local s = string.format('%03d', az) .. ' for ' .. dist
+
+ if alt then
+ if metric then
+ s = s .. ' at ' .. mist.utils.round(alt, 0)
+ else
+ s = s .. ' at ' .. mist.utils.round(mist.utils.metersToFeet(alt), 0)
+ end
+ end
+ return s
+ end
+
+ function mist.getNorthCorrection(gPoint) --gets the correction needed for true north
+ local point = mist.utils.deepCopy(gPoint)
+ if not point.z then --Vec2; convert to Vec3
+ point.z = point.y
+ point.y = 0
+ end
+ local lat, lon = coord.LOtoLL(point)
+ local north_posit = coord.LLtoLO(lat + 1, lon)
+ return math.atan2(north_posit.z - point.z, north_posit.x - point.x)
+ end
+
+ --- Returns skill of the given unit.
+ -- @tparam string unitName unit name
+ -- @return skill of the unit
+ function mist.getUnitSkill(unitName)
+ if mist.DBs.unitsByName[unitName] then
+ if Unit.getByName(unitName) then
+ local lunit = Unit.getByName(unitName)
+ local data = mist.DBs.unitsByName[unitName]
+ if data.unitName == unitName and data.type == lunit:getTypeName() and data.unitId == tonumber(lunit:getID()) and data.skill then
+ return data.skill
+ end
+ end
+ end
+ log:error("Unit not found in DB: $1", unitName)
+ return false
+ end
+
+ --- Returns an array containing a group's units positions.
+ -- e.g.
+ -- {
+ -- [1] = {x = 299435.224, y = -1146632.6773},
+ -- [2] = {x = 663324.6563, y = 322424.1112}
+ -- }
+ -- @tparam number|string groupIdent group id or name
+ -- @treturn table array containing positions of each group member
+ function mist.getGroupPoints(groupIdent)
+ -- search by groupId and allow groupId and groupName as inputs
+ local gpId = groupIdent
+ if type(groupIdent) == 'string' and not tonumber(groupIdent) then
+ if mist.DBs.MEgroupsByName[groupIdent] then
+ gpId = mist.DBs.MEgroupsByName[groupIdent].groupId
+ else
+ log:error("Group not found in mist.DBs.MEgroupsByName: $1", groupIdent)
+ end
+ end
+
+ for coa_name, coa_data in pairs(env.mission.coalition) do
+ if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then
+ if coa_data.country then --there is a country table
+ for cntry_id, cntry_data in pairs(coa_data.country) do
+ for obj_type_name, obj_type_data in pairs(cntry_data) do
+ if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points
+ if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group!
+ for group_num, group_data in pairs(obj_type_data.group) do
+ if group_data and group_data.groupId == gpId then -- this is the group we are looking for
+ if group_data.route and group_data.route.points and #group_data.route.points > 0 then
+ local points = {}
+ for point_num, point in pairs(group_data.route.points) do
+ if not point.point then
+ points[point_num] = { x = point.x, y = point.y }
+ else
+ points[point_num] = point.point --it's possible that the ME could move to the point = Vec2 notation.
+ end
+ end
+ return points
+ end
+ return
+ end --if group_data and group_data.name and group_data.name == 'groupname'
+ end --for group_num, group_data in pairs(obj_type_data.group) do
+ end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then
+ end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then
+ end --for obj_type_name, obj_type_data in pairs(cntry_data) do
+ end --for cntry_id, cntry_data in pairs(coa_data.country) do
+ end --if coa_data.country then --there is a country table
+ end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then
+ end --for coa_name, coa_data in pairs(mission.coalition) do
+ end
+
+ --- getUnitAttitude(unit) return values.
+ -- Yaw, AoA, ClimbAngle - relative to earth reference
+ -- DOES NOT TAKE INTO ACCOUNT WIND.
+ -- @table attitude
+ -- @tfield number Heading in radians, range of 0 to 2*pi,
+ -- relative to true north.
+ -- @tfield number Pitch in radians, range of -pi/2 to pi/2
+ -- @tfield number Roll in radians, range of 0 to 2*pi,
+ -- right roll is positive direction.
+ -- @tfield number Yaw in radians, range of -pi to pi,
+ -- right yaw is positive direction.
+ -- @tfield number AoA in radians, range of -pi to pi,
+ -- rotation of aircraft to the right in comparison to
+ -- flight direction being positive.
+ -- @tfield number ClimbAngle in radians, range of -pi/2 to pi/2
+
+ --- Returns the attitude of a given unit.
+ -- Will work on any unit, even if not an aircraft.
+ -- @tparam Unit unit unit whose attitude is returned.
+ -- @treturn table @{attitude}
+ function mist.getAttitude(unit)
+ local unitpos = unit:getPosition()
+ if unitpos then
+
+ local Heading = math.atan2(unitpos.x.z, unitpos.x.x)
+
+ Heading = Heading + mist.getNorthCorrection(unitpos.p)
+
+ if Heading < 0 then
+ Heading = Heading + 2*math.pi -- put heading in range of 0 to 2*pi
+ end
+ ---- heading complete.----
+
+ local Pitch = math.asin(unitpos.x.y)
+ ---- pitch complete.----
+
+ -- now get roll:
+ --maybe not the best way to do it, but it works.
+
+ --first, make a vector that is perpendicular to y and unitpos.x with cross product
+ local cp = mist.vec.cp(unitpos.x, {x = 0, y = 1, z = 0})
+
+ --now, get dot product of of this cross product with unitpos.z
+ local dp = mist.vec.dp(cp, unitpos.z)
+
+ --now get the magnitude of the roll (magnitude of the angle between two vectors is acos(vec1.vec2/|vec1||vec2|)
+ local Roll = math.acos(dp/(mist.vec.mag(cp)*mist.vec.mag(unitpos.z)))
+
+ --now, have to get sign of roll.
+ -- by convention, making right roll positive
+ -- to get sign of roll, use the y component of unitpos.z. For right roll, y component is negative.
+
+ if unitpos.z.y > 0 then -- left roll, flip the sign of the roll
+ Roll = -Roll
+ end
+ ---- roll complete. ----
+
+ --now, work on yaw, AoA, climb, and abs velocity
+ local Yaw
+ local AoA
+ local ClimbAngle
+
+ -- get unit velocity
+ local unitvel = unit:getVelocity()
+ if mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity!
+ local AxialVel = {} --unit velocity transformed into aircraft axes directions
+
+ --transform velocity components in direction of aircraft axes.
+ AxialVel.x = mist.vec.dp(unitpos.x, unitvel)
+ AxialVel.y = mist.vec.dp(unitpos.y, unitvel)
+ AxialVel.z = mist.vec.dp(unitpos.z, unitvel)
+
+ --Yaw is the angle between unitpos.x and the x and z velocities
+ --define right yaw as positive
+ Yaw = math.acos(mist.vec.dp({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = 0, z = AxialVel.z})/mist.vec.mag({x = AxialVel.x, y = 0, z = AxialVel.z}))
+
+ --now set correct direction:
+ if AxialVel.z > 0 then
+ Yaw = -Yaw
+ end
+
+ -- AoA is angle between unitpos.x and the x and y velocities
+ AoA = math.acos(mist.vec.dp({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = AxialVel.y, z = 0})/mist.vec.mag({x = AxialVel.x, y = AxialVel.y, z = 0}))
+
+ --now set correct direction:
+ if AxialVel.y > 0 then
+ AoA = -AoA
+ end
+
+ ClimbAngle = math.asin(unitvel.y/mist.vec.mag(unitvel))
+ end
+ return { Heading = Heading, Pitch = Pitch, Roll = Roll, Yaw = Yaw, AoA = AoA, ClimbAngle = ClimbAngle}
+ else
+ log:error("Couldn't get unit's position")
+ end
+ end
+
+ --- Returns heading of given unit.
+ -- @tparam Unit unit unit whose heading is returned.
+ -- @param rawHeading
+ -- @treturn number heading of the unit, in range
+ -- of 0 to 2*pi.
+ function mist.getHeading(unit, rawHeading)
+ local unitpos = unit:getPosition()
+ if unitpos then
+ local Heading = math.atan2(unitpos.x.z, unitpos.x.x)
+ if not rawHeading then
+ Heading = Heading + mist.getNorthCorrection(unitpos.p)
+ end
+ if Heading < 0 then
+ Heading = Heading + 2*math.pi -- put heading in range of 0 to 2*pi
+ end
+ return Heading
+ end
+ end
+
+ --- Returns given unit's pitch
+ -- @tparam Unit unit unit whose pitch is returned.
+ -- @treturn number pitch of given unit
+ function mist.getPitch(unit)
+ local unitpos = unit:getPosition()
+ if unitpos then
+ return math.asin(unitpos.x.y)
+ end
+ end
+
+ --- Returns given unit's roll.
+ -- @tparam Unit unit unit whose roll is returned.
+ -- @treturn number roll of given unit
+ function mist.getRoll(unit)
+ local unitpos = unit:getPosition()
+ if unitpos then
+ -- now get roll:
+ --maybe not the best way to do it, but it works.
+
+ --first, make a vector that is perpendicular to y and unitpos.x with cross product
+ local cp = mist.vec.cp(unitpos.x, {x = 0, y = 1, z = 0})
+
+ --now, get dot product of of this cross product with unitpos.z
+ local dp = mist.vec.dp(cp, unitpos.z)
+
+ --now get the magnitude of the roll (magnitude of the angle between two vectors is acos(vec1.vec2/|vec1||vec2|)
+ local Roll = math.acos(dp/(mist.vec.mag(cp)*mist.vec.mag(unitpos.z)))
+
+ --now, have to get sign of roll.
+ -- by convention, making right roll positive
+ -- to get sign of roll, use the y component of unitpos.z. For right roll, y component is negative.
+
+ if unitpos.z.y > 0 then -- left roll, flip the sign of the roll
+ Roll = -Roll
+ end
+ return Roll
+ end
+ end
+
+ --- Returns given unit's yaw.
+ -- @tparam Unit unit unit whose yaw is returned.
+ -- @treturn number yaw of given unit.
+ function mist.getYaw(unit)
+ local unitpos = unit:getPosition()
+ if unitpos then
+ -- get unit velocity
+ local unitvel = unit:getVelocity()
+ if mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity!
+ local AxialVel = {} --unit velocity transformed into aircraft axes directions
+
+ --transform velocity components in direction of aircraft axes.
+ AxialVel.x = mist.vec.dp(unitpos.x, unitvel)
+ AxialVel.y = mist.vec.dp(unitpos.y, unitvel)
+ AxialVel.z = mist.vec.dp(unitpos.z, unitvel)
+
+ --Yaw is the angle between unitpos.x and the x and z velocities
+ --define right yaw as positive
+ local Yaw = math.acos(mist.vec.dp({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = 0, z = AxialVel.z})/mist.vec.mag({x = AxialVel.x, y = 0, z = AxialVel.z}))
+
+ --now set correct direction:
+ if AxialVel.z > 0 then
+ Yaw = -Yaw
+ end
+ return Yaw
+ end
+ end
+ end
+
+ --- Returns given unit's angle of attack.
+ -- @tparam Unit unit unit to get AoA from.
+ -- @treturn number angle of attack of the given unit.
+ function mist.getAoA(unit)
+ local unitpos = unit:getPosition()
+ if unitpos then
+ local unitvel = unit:getVelocity()
+ if mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity!
+ local AxialVel = {} --unit velocity transformed into aircraft axes directions
+
+ --transform velocity components in direction of aircraft axes.
+ AxialVel.x = mist.vec.dp(unitpos.x, unitvel)
+ AxialVel.y = mist.vec.dp(unitpos.y, unitvel)
+ AxialVel.z = mist.vec.dp(unitpos.z, unitvel)
+
+ -- AoA is angle between unitpos.x and the x and y velocities
+ local AoA = math.acos(mist.vec.dp({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = AxialVel.y, z = 0})/mist.vec.mag({x = AxialVel.x, y = AxialVel.y, z = 0}))
+
+ --now set correct direction:
+ if AxialVel.y > 0 then
+ AoA = -AoA
+ end
+ return AoA
+ end
+ end
+ end
+
+ --- Returns given unit's climb angle.
+ -- @tparam Unit unit unit to get climb angle from.
+ -- @treturn number climb angle of given unit.
+ function mist.getClimbAngle(unit)
+ local unitpos = unit:getPosition()
+ if unitpos then
+ local unitvel = unit:getVelocity()
+ if mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity!
+ return math.asin(unitvel.y/mist.vec.mag(unitvel))
+ end
+ end
+ end
+
+ --[[--
+ Unit name table.
+ Many Mist functions require tables of unit names, which are known
+ in Mist as UnitNameTables. These follow a special set of shortcuts
+ borrowed from Slmod. These shortcuts alleviate the problem of entering
+ huge lists of unit names by hand, and in many cases, they remove the
+ need to even know the names of the units in the first place!
+
+ These are the unit table "short-cut" commands:
+
+ Prefixes:
+ "[-u]" - subtract this unit if its in the table
+ "[g]" - add this group to the table
+ "[-g]" - subtract this group from the table
+ "[c]" - add this country's units
+ "[-c]" - subtract this country's units if any are in the table
+
+ Stand-alone identifiers
+ "[all]" - add all units
+ "[-all]" - subtract all units (not very useful by itself)
+ "[blue]" - add all blue units
+ "[-blue]" - subtract all blue units
+ "[red]" - add all red coalition units
+ "[-red]" - subtract all red units
+
+ Compound Identifiers:
+ "[c][helicopter]" - add all of this country's helicopters
+ "[-c][helicopter]" - subtract all of this country's helicopters
+ "[c][plane]" - add all of this country's planes
+ "[-c][plane]" - subtract all of this country's planes
+ "[c][ship]" - add all of this country's ships
+ "[-c][ship]" - subtract all of this country's ships
+ "[c][vehicle]" - add all of this country's vehicles
+ "[-c][vehicle]" - subtract all of this country's vehicles
+
+ "[all][helicopter]" - add all helicopters
+ "[-all][helicopter]" - subtract all helicopters
+ "[all][plane]" - add all planes
+ "[-all][plane]" - subtract all planes
+ "[all][ship]" - add all ships
+ "[-all][ship]" - subtract all ships
+ "[all][vehicle]" - add all vehicles
+ "[-all][vehicle]" - subtract all vehicles
+
+ "[blue][helicopter]" - add all blue coalition helicopters
+ "[-blue][helicopter]" - subtract all blue coalition helicopters
+ "[blue][plane]" - add all blue coalition planes
+ "[-blue][plane]" - subtract all blue coalition planes
+ "[blue][ship]" - add all blue coalition ships
+ "[-blue][ship]" - subtract all blue coalition ships
+ "[blue][vehicle]" - add all blue coalition vehicles
+ "[-blue][vehicle]" - subtract all blue coalition vehicles
+
+ "[red][helicopter]" - add all red coalition helicopters
+ "[-red][helicopter]" - subtract all red coalition helicopters
+ "[red][plane]" - add all red coalition planes
+ "[-red][plane]" - subtract all red coalition planes
+ "[red][ship]" - add all red coalition ships
+ "[-red][ship]" - subtract all red coalition ships
+ "[red][vehicle]" - add all red coalition vehicles
+ "[-red][vehicle]" - subtract all red coalition vehicles
+
+ Country names to be used in [c] and [-c] short-cuts:
+ Turkey
+ Norway
+ The Netherlands
+ Spain
+ 11
+ UK
+ Denmark
+ USA
+ Georgia
+ Germany
+ Belgium
+ Canada
+ France
+ Israel
+ Ukraine
+ Russia
+ South Ossetia
+ Abkhazia
+ Italy
+ Australia
+ Austria
+ Belarus
+ Bulgaria
+ Czech Republic
+ China
+ Croatia
+ Finland
+ Greece
+ Hungary
+ India
+ Iran
+ Iraq
+ Japan
+ Kazakhstan
+ North Korea
+ Pakistan
+ Poland
+ Romania
+ Saudi Arabia
+ Serbia, Slovakia
+ South Korea
+ Sweden
+ Switzerland
+ Syria
+ USAF Aggressors
+
+ Do NOT use a '[u]' notation for single units. Single units are referenced
+ the same way as before: Simply input their names as strings.
+
+ These unit tables are evaluated in order, and you cannot subtract a unit
+ from a table before it is added. For example:
+
+ {'[blue]', '[-c]Georgia'}
+
+ will evaluate to all of blue coalition except those units owned by the
+ country named "Georgia"; however:
+
+ {'[-c]Georgia', '[blue]'}
+
+ will evaluate to all of the units in blue coalition, because the addition
+ of all units owned by blue coalition occurred AFTER the subtraction of all
+ units owned by Georgia (which actually subtracted nothing at all, since
+ there were no units in the table when the subtraction occurred).
+
+ More examples:
+
+ {'[blue][plane]', '[-c]Georgia', '[-g]Hawg 1'}
+
+ Evaluates to all blue planes, except those blue units owned by the country
+ named "Georgia" and the units in the group named "Hawg1".
+
+
+ {'[g]arty1', '[g]arty2', '[-u]arty1_AD', '[-u]arty2_AD', 'Shark 11' }
+
+ Evaluates to the unit named "Shark 11", plus all the units in groups named
+ "arty1" and "arty2" except those that are named "arty1\_AD" and "arty2\_AD".
+
+ @table UnitNameTable
+ ]]
+
+ --- Returns a table containing unit names.
+ -- @tparam table tbl sequential strings
+ -- @treturn table @{UnitNameTable}
+ function mist.makeUnitTable(tbl)
+ --Assumption: will be passed a table of strings, sequential
+ log:info(tbl)
+ local units_by_name = {}
+
+ local l_munits = mist.DBs.units --local reference for faster execution
+ for i = 1, #tbl do
+ local unit = tbl[i]
+ if unit:sub(1,4) == '[-u]' then --subtract a unit
+ if units_by_name[unit:sub(5)] then -- 5 to end
+ units_by_name[unit:sub(5)] = nil --remove
+ end
+ elseif unit:sub(1,3) == '[g]' then -- add a group
+ for coa, coa_tbl in pairs(l_munits) do
+ for country, country_table in pairs(coa_tbl) do
+ for unit_type, unit_type_tbl in pairs(country_table) do
+ if type(unit_type_tbl) == 'table' then
+ for group_ind, group_tbl in pairs(unit_type_tbl) do
+ if type(group_tbl) == 'table' and group_tbl.groupName == unit:sub(4) then
+ -- index 4 to end
+ for unit_ind, unit in pairs(group_tbl.units) do
+ units_by_name[unit.unitName] = true --add
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ elseif unit:sub(1,4) == '[-g]' then -- subtract a group
+ for coa, coa_tbl in pairs(l_munits) do
+ for country, country_table in pairs(coa_tbl) do
+ for unit_type, unit_type_tbl in pairs(country_table) do
+ if type(unit_type_tbl) == 'table' then
+ for group_ind, group_tbl in pairs(unit_type_tbl) do
+ if type(group_tbl) == 'table' and group_tbl.groupName == unit:sub(5) then
+ -- index 5 to end
+ for unit_ind, unit in pairs(group_tbl.units) do
+ if units_by_name[unit.unitName] then
+ units_by_name[unit.unitName] = nil --remove
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ elseif unit:sub(1,3) == '[c]' then -- add a country
+ local category = ''
+ local country_start = 4
+ if unit:sub(4,15) == '[helicopter]' then
+ category = 'helicopter'
+ country_start = 16
+ elseif unit:sub(4,10) == '[plane]' then
+ category = 'plane'
+ country_start = 11
+ elseif unit:sub(4,9) == '[ship]' then
+ category = 'ship'
+ country_start = 10
+ elseif unit:sub(4,12) == '[vehicle]' then
+ category = 'vehicle'
+ country_start = 13
+ end
+ for coa, coa_tbl in pairs(l_munits) do
+ for country, country_table in pairs(coa_tbl) do
+ if country == string.lower(unit:sub(country_start)) then -- match
+ for unit_type, unit_type_tbl in pairs(country_table) do
+ if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then
+ for group_ind, group_tbl in pairs(unit_type_tbl) do
+ if type(group_tbl) == 'table' then
+ for unit_ind, unit in pairs(group_tbl.units) do
+ units_by_name[unit.unitName] = true --add
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ elseif unit:sub(1,4) == '[-c]' then -- subtract a country
+ local category = ''
+ local country_start = 5
+ if unit:sub(5,16) == '[helicopter]' then
+ category = 'helicopter'
+ country_start = 17
+ elseif unit:sub(5,11) == '[plane]' then
+ category = 'plane'
+ country_start = 12
+ elseif unit:sub(5,10) == '[ship]' then
+ category = 'ship'
+ country_start = 11
+ elseif unit:sub(5,13) == '[vehicle]' then
+ category = 'vehicle'
+ country_start = 14
+ end
+ for coa, coa_tbl in pairs(l_munits) do
+ for country, country_table in pairs(coa_tbl) do
+ if country == string.lower(unit:sub(country_start)) then -- match
+ for unit_type, unit_type_tbl in pairs(country_table) do
+ if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then
+ for group_ind, group_tbl in pairs(unit_type_tbl) do
+ if type(group_tbl) == 'table' then
+ for unit_ind, unit in pairs(group_tbl.units) do
+ if units_by_name[unit.unitName] then
+ units_by_name[unit.unitName] = nil --remove
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ elseif unit:sub(1,6) == '[blue]' then -- add blue coalition
+ local category = ''
+ if unit:sub(7) == '[helicopter]' then
+ category = 'helicopter'
+ elseif unit:sub(7) == '[plane]' then
+ category = 'plane'
+ elseif unit:sub(7) == '[ship]' then
+ category = 'ship'
+ elseif unit:sub(7) == '[vehicle]' then
+ category = 'vehicle'
+ end
+ for coa, coa_tbl in pairs(l_munits) do
+ if coa == 'blue' then
+ for country, country_table in pairs(coa_tbl) do
+ for unit_type, unit_type_tbl in pairs(country_table) do
+ if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then
+ for group_ind, group_tbl in pairs(unit_type_tbl) do
+ if type(group_tbl) == 'table' then
+ for unit_ind, unit in pairs(group_tbl.units) do
+ units_by_name[unit.unitName] = true --add
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ elseif unit:sub(1,7) == '[-blue]' then -- subtract blue coalition
+ local category = ''
+ if unit:sub(8) == '[helicopter]' then
+ category = 'helicopter'
+ elseif unit:sub(8) == '[plane]' then
+ category = 'plane'
+ elseif unit:sub(8) == '[ship]' then
+ category = 'ship'
+ elseif unit:sub(8) == '[vehicle]' then
+ category = 'vehicle'
+ end
+ for coa, coa_tbl in pairs(l_munits) do
+ if coa == 'blue' then
+ for country, country_table in pairs(coa_tbl) do
+ for unit_type, unit_type_tbl in pairs(country_table) do
+ if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then
+ for group_ind, group_tbl in pairs(unit_type_tbl) do
+ if type(group_tbl) == 'table' then
+ for unit_ind, unit in pairs(group_tbl.units) do
+ if units_by_name[unit.unitName] then
+ units_by_name[unit.unitName] = nil --remove
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ elseif unit:sub(1,5) == '[red]' then -- add red coalition
+ local category = ''
+ if unit:sub(6) == '[helicopter]' then
+ category = 'helicopter'
+ elseif unit:sub(6) == '[plane]' then
+ category = 'plane'
+ elseif unit:sub(6) == '[ship]' then
+ category = 'ship'
+ elseif unit:sub(6) == '[vehicle]' then
+ category = 'vehicle'
+ end
+ for coa, coa_tbl in pairs(l_munits) do
+ if coa == 'red' then
+ for country, country_table in pairs(coa_tbl) do
+ for unit_type, unit_type_tbl in pairs(country_table) do
+ if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then
+ for group_ind, group_tbl in pairs(unit_type_tbl) do
+ if type(group_tbl) == 'table' then
+ for unit_ind, unit in pairs(group_tbl.units) do
+ units_by_name[unit.unitName] = true --add
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ elseif unit:sub(1,6) == '[-red]' then -- subtract red coalition
+ local category = ''
+ if unit:sub(7) == '[helicopter]' then
+ category = 'helicopter'
+ elseif unit:sub(7) == '[plane]' then
+ category = 'plane'
+ elseif unit:sub(7) == '[ship]' then
+ category = 'ship'
+ elseif unit:sub(7) == '[vehicle]' then
+ category = 'vehicle'
+ end
+ for coa, coa_tbl in pairs(l_munits) do
+ if coa == 'red' then
+ for country, country_table in pairs(coa_tbl) do
+ for unit_type, unit_type_tbl in pairs(country_table) do
+ if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then
+ for group_ind, group_tbl in pairs(unit_type_tbl) do
+ if type(group_tbl) == 'table' then
+ for unit_ind, unit in pairs(group_tbl.units) do
+ if units_by_name[unit.unitName] then
+ units_by_name[unit.unitName] = nil --remove
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ elseif unit:sub(1,5) == '[all]' then -- add all of a certain category (or all categories)
+ local category = ''
+ if unit:sub(6) == '[helicopter]' then
+ category = 'helicopter'
+ elseif unit:sub(6) == '[plane]' then
+ category = 'plane'
+ elseif unit:sub(6) == '[ship]' then
+ category = 'ship'
+ elseif unit:sub(6) == '[vehicle]' then
+ category = 'vehicle'
+ end
+ for coa, coa_tbl in pairs(l_munits) do
+ for country, country_table in pairs(coa_tbl) do
+ for unit_type, unit_type_tbl in pairs(country_table) do
+ if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then
+ for group_ind, group_tbl in pairs(unit_type_tbl) do
+ if type(group_tbl) == 'table' then
+ for unit_ind, unit in pairs(group_tbl.units) do
+ units_by_name[unit.unitName] = true --add
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ elseif unit:sub(1,6) == '[-all]' then -- subtract all of a certain category (or all categories)
+ local category = ''
+ if unit:sub(7) == '[helicopter]' then
+ category = 'helicopter'
+ elseif unit:sub(7) == '[plane]' then
+ category = 'plane'
+ elseif unit:sub(7) == '[ship]' then
+ category = 'ship'
+ elseif unit:sub(7) == '[vehicle]' then
+ category = 'vehicle'
+ end
+ for coa, coa_tbl in pairs(l_munits) do
+ for country, country_table in pairs(coa_tbl) do
+ for unit_type, unit_type_tbl in pairs(country_table) do
+ if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then
+ for group_ind, group_tbl in pairs(unit_type_tbl) do
+ if type(group_tbl) == 'table' then
+ for unit_ind, unit in pairs(group_tbl.units) do
+ if units_by_name[unit.unitName] then
+ units_by_name[unit.unitName] = nil --remove
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ else -- just a regular unit
+ units_by_name[unit] = true --add
+ end
+ end
+
+ local units_tbl = {} -- indexed sequentially
+ for unit_name, val in pairs(units_by_name) do
+ if val then
+ units_tbl[#units_tbl + 1] = unit_name -- add all the units to the table
+ end
+ end
+
+
+ units_tbl.processed = timer.getTime() --add the processed flag
+ return units_tbl
+end
+
+function mist.getDeadMapObjsInZones(zone_names)
+ -- zone_names: table of zone names
+ -- returns: table of dead map objects (indexed numerically)
+ local map_objs = {}
+ local zones = {}
+ for i = 1, #zone_names do
+ if mist.DBs.zonesByName[zone_names[i]] then
+ zones[#zones + 1] = mist.DBs.zonesByName[zone_names[i]]
+ end
+ end
+ for obj_id, obj in pairs(mist.DBs.deadObjects) do
+ if obj.objectType and obj.objectType == 'building' then --dead map object
+ for i = 1, #zones do
+ if ((zones[i].point.x - obj.objectPos.x)^2 + (zones[i].point.z - obj.objectPos.z)^2)^0.5 <= zones[i].radius then
+ map_objs[#map_objs + 1] = mist.utils.deepCopy(obj)
+ end
+ end
+ end
+ end
+ return map_objs
+end
+
+function mist.getDeadMapObjsInPolygonZone(zone)
+ -- zone_names: table of zone names
+ -- returns: table of dead map objects (indexed numerically)
+ local map_objs = {}
+ for obj_id, obj in pairs(mist.DBs.deadObjects) do
+ if obj.objectType and obj.objectType == 'building' then --dead map object
+ if mist.pointInPolygon(obj.objectPos, zone) then
+ map_objs[#map_objs + 1] = mist.utils.deepCopy(obj)
+ end
+ end
+ end
+ return map_objs
+end
+
+function mist.pointInPolygon(point, poly, maxalt) --raycasting point in polygon. Code from http://softsurfer.com/Archive/algorithm_0103/algorithm_0103.htm
+ --[[local type_tbl = {
+ point = {'table'},
+ poly = {'table'},
+ maxalt = {'number', 'nil'},
+ }
+
+ local err, errmsg = mist.utils.typeCheck('mist.pointInPolygon', type_tbl, {point, poly, maxalt})
+ assert(err, errmsg)
+ ]]
+ point = mist.utils.makeVec3(point)
+ local px = point.x
+ local pz = point.z
+ local cn = 0
+ local newpoly = mist.utils.deepCopy(poly)
+
+ if not maxalt or (point.y <= maxalt) then
+ local polysize = #newpoly
+ newpoly[#newpoly + 1] = newpoly[1]
+
+ newpoly[1] = mist.utils.makeVec3(newpoly[1])
+
+ for k = 1, polysize do
+ newpoly[k+1] = mist.utils.makeVec3(newpoly[k+1])
+ if ((newpoly[k].z <= pz) and (newpoly[k+1].z > pz)) or ((newpoly[k].z > pz) and (newpoly[k+1].z <= pz)) then
+ local vt = (pz - newpoly[k].z) / (newpoly[k+1].z - newpoly[k].z)
+ if (px < newpoly[k].x + vt*(newpoly[k+1].x - newpoly[k].x)) then
+ cn = cn + 1
+ end
+ end
+ end
+
+ return cn%2 == 1
+ else
+ return false
+ end
+end
+
+function mist.getUnitsInPolygon(unit_names, polyZone, max_alt)
+ local units = {}
+
+ for i = 1, #unit_names do
+ units[#units + 1] = Unit.getByName(unitNames[i])
+ end
+
+ local inZoneUnits = {}
+ for i =1, #units do
+ if units[i]:isActive() and mist.pointInPolygon(units[i]:getPosition().p, polyZone, max_alt) then
+ inZoneUnits[inZoneUnits + 1] = units[i]
+ end
+ end
+
+ return inZoneUnits
+end
+
+function mist.getUnitsInZones(unit_names, zone_names, zone_type)
+
+ zone_type = zone_type or 'cylinder'
+ if zone_type == 'c' or zone_type == 'cylindrical' or zone_type == 'C' then
+ zone_type = 'cylinder'
+ end
+ if zone_type == 's' or zone_type == 'spherical' or zone_type == 'S' then
+ zone_type = 'sphere'
+ end
+
+ assert(zone_type == 'cylinder' or zone_type == 'sphere', 'invalid zone_type: ' .. tostring(zone_type))
+
+ local units = {}
+ local zones = {}
+
+ for k = 1, #unit_names do
+ local unit = Unit.getByName(unit_names[k])
+ if unit then
+ units[#units + 1] = unit
+ end
+ end
+
+
+ for k = 1, #zone_names do
+ local zone = trigger.misc.getZone(zone_names[k])
+ if zone then
+ zones[#zones + 1] = {radius = zone.radius, x = zone.point.x, y = zone.point.y, z = zone.point.z}
+ end
+ end
+
+ local in_zone_units = {}
+
+ for units_ind = 1, #units do
+ for zones_ind = 1, #zones do
+ if zone_type == 'sphere' then --add land height value for sphere zone type
+ local alt = land.getHeight({x = zones[zones_ind].x, y = zones[zones_ind].z})
+ if alt then
+ zones[zones_ind].y = alt
+ end
+ end
+ local unit_pos = units[units_ind]:getPosition().p
+ if unit_pos and units[units_ind]:isActive() == true then
+ if zone_type == 'cylinder' and (((unit_pos.x - zones[zones_ind].x)^2 + (unit_pos.z - zones[zones_ind].z)^2)^0.5 <= zones[zones_ind].radius) then
+ in_zone_units[#in_zone_units + 1] = units[units_ind]
+ break
+ elseif zone_type == 'sphere' and (((unit_pos.x - zones[zones_ind].x)^2 + (unit_pos.y - zones[zones_ind].y)^2 + (unit_pos.z - zones[zones_ind].z)^2)^0.5 <= zones[zones_ind].radius) then
+ in_zone_units[#in_zone_units + 1] = units[units_ind]
+ break
+ end
+ end
+ end
+ end
+ return in_zone_units
+end
+
+function mist.getUnitsInMovingZones(unit_names, zone_unit_names, radius, zone_type)
+
+ zone_type = zone_type or 'cylinder'
+ if zone_type == 'c' or zone_type == 'cylindrical' or zone_type == 'C' then
+ zone_type = 'cylinder'
+ end
+ if zone_type == 's' or zone_type == 'spherical' or zone_type == 'S' then
+ zone_type = 'sphere'
+ end
+
+ assert(zone_type == 'cylinder' or zone_type == 'sphere', 'invalid zone_type: ' .. tostring(zone_type))
+
+ local units = {}
+ local zone_units = {}
+
+ for k = 1, #unit_names do
+ local unit = Unit.getByName(unit_names[k])
+ if unit then
+ units[#units + 1] = unit
+ end
+ end
+
+ for k = 1, #zone_unit_names do
+ local unit = Unit.getByName(zone_unit_names[k])
+ if unit then
+ zone_units[#zone_units + 1] = unit
+ end
+ end
+
+ local in_zone_units = {}
+
+ for units_ind = 1, #units do
+ for zone_units_ind = 1, #zone_units do
+ local unit_pos = units[units_ind]:getPosition().p
+ local zone_unit_pos = zone_units[zone_units_ind]:getPosition().p
+ if unit_pos and zone_unit_pos and units[units_ind]:isActive() == true then
+ if zone_type == 'cylinder' and (((unit_pos.x - zone_unit_pos.x)^2 + (unit_pos.z - zone_unit_pos.z)^2)^0.5 <= radius) then
+ in_zone_units[#in_zone_units + 1] = units[units_ind]
+ break
+ elseif zone_type == 'sphere' and (((unit_pos.x - zone_unit_pos.x)^2 + (unit_pos.y - zone_unit_pos.y)^2 + (unit_pos.z - zone_unit_pos.z)^2)^0.5 <= radius) then
+ in_zone_units[#in_zone_units + 1] = units[units_ind]
+ break
+ end
+ end
+ end
+ end
+ return in_zone_units
+end
+
+function mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius)
+ log:info("$1, $2, $3, $4, $5", unitset1, altoffset1, unitset2, altoffset2, radius)
+ radius = radius or math.huge
+ local unit_info1 = {}
+ local unit_info2 = {}
+
+ -- get the positions all in one step, saves execution time.
+ for unitset1_ind = 1, #unitset1 do
+ local unit1 = Unit.getByName(unitset1[unitset1_ind])
+ if unit1 and unit1:isActive() == true then
+ unit_info1[#unit_info1 + 1] = {}
+ unit_info1[#unit_info1].unit = unit1
+ unit_info1[#unit_info1].pos = unit1:getPosition().p
+ end
+ end
+
+ for unitset2_ind = 1, #unitset2 do
+ local unit2 = Unit.getByName(unitset2[unitset2_ind])
+ if unit2 and unit2:isActive() == true then
+ unit_info2[#unit_info2 + 1] = {}
+ unit_info2[#unit_info2].unit = unit2
+ unit_info2[#unit_info2].pos = unit2:getPosition().p
+ end
+ end
+
+ local LOS_data = {}
+ -- now compute los
+ for unit1_ind = 1, #unit_info1 do
+ local unit_added = false
+ for unit2_ind = 1, #unit_info2 do
+ if radius == math.huge or (mist.vec.mag(mist.vec.sub(unit_info1[unit1_ind].pos, unit_info2[unit2_ind].pos)) < radius) then -- inside radius
+ local point1 = { x = unit_info1[unit1_ind].pos.x, y = unit_info1[unit1_ind].pos.y + altoffset1, z = unit_info1[unit1_ind].pos.z}
+ local point2 = { x = unit_info2[unit2_ind].pos.x, y = unit_info2[unit2_ind].pos.y + altoffset2, z = unit_info2[unit2_ind].pos.z}
+ if land.isVisible(point1, point2) then
+ if unit_added == false then
+ unit_added = true
+ LOS_data[#LOS_data + 1] = {}
+ LOS_data[#LOS_data].unit = unit_info1[unit1_ind].unit
+ LOS_data[#LOS_data].vis = {}
+ LOS_data[#LOS_data].vis[#LOS_data[#LOS_data].vis + 1] = unit_info2[unit2_ind].unit
+ else
+ LOS_data[#LOS_data].vis[#LOS_data[#LOS_data].vis + 1] = unit_info2[unit2_ind].unit
+ end
+ end
+ end
+ end
+ end
+
+ return LOS_data
+end
+
+function mist.getAvgPoint(points)
+ local avgX, avgY, avgZ, totNum = 0, 0, 0, 0
+ for i = 1, #points do
+ local nPoint = mist.utils.makeVec3(points[i])
+ if nPoint.z then
+ avgX = avgX + nPoint.x
+ avgY = avgY + nPoint.y
+ avgZ = avgZ + nPoint.z
+ totNum = totNum + 1
+ end
+ end
+ if totNum ~= 0 then
+ return {x = avgX/totNum, y = avgY/totNum, z = avgZ/totNum}
+ end
+end
+
+--Gets the average position of a group of units (by name)
+function mist.getAvgPos(unitNames)
+ local avgX, avgY, avgZ, totNum = 0, 0, 0, 0
+ for i = 1, #unitNames do
+ local unit
+ if Unit.getByName(unitNames[i]) then
+ unit = Unit.getByName(unitNames[i])
+ elseif StaticObject.getByName(unitNames[i]) then
+ unit = StaticObject.getByName(unitNames[i])
+ end
+ if unit then
+ local pos = unit:getPosition().p
+ if pos then -- you never know O.o
+ avgX = avgX + pos.x
+ avgY = avgY + pos.y
+ avgZ = avgZ + pos.z
+ totNum = totNum + 1
+ end
+ end
+ end
+ if totNum ~= 0 then
+ return {x = avgX/totNum, y = avgY/totNum, z = avgZ/totNum}
+ end
+end
+
+function mist.getAvgGroupPos(groupName)
+ if type(groupName) == 'string' and Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then
+ groupName = Group.getByName(groupName)
+ end
+ local units = {}
+ for i = 1, #groupName:getSize() do
+ table.insert(units, groupName.getUnit(i):getName())
+ end
+
+ return mist.getAvgPos(units)
+
+end
+
+--[[ vars for mist.getMGRSString:
+vars.units - table of unit names (NOT unitNameTable- maybe this should change).
+vars.acc - integer between 0 and 5, inclusive
+]]
+function mist.getMGRSString(vars)
+ local units = vars.units
+ local acc = vars.acc or 5
+ local avgPos = mist.getAvgPos(units)
+ if avgPos then
+ return mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(avgPos)), acc)
+ end
+end
+
+--[[ vars for mist.getLLString
+vars.units - table of unit names (NOT unitNameTable- maybe this should change).
+vars.acc - integer, number of numbers after decimal place
+vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes.
+]]
+function mist.getLLString(vars)
+ local units = vars.units
+ local acc = vars.acc or 3
+ local DMS = vars.DMS
+ local avgPos = mist.getAvgPos(units)
+ if avgPos then
+ local lat, lon = coord.LOtoLL(avgPos)
+ return mist.tostringLL(lat, lon, acc, DMS)
+ end
+end
+
+--[[
+vars.units- table of unit names (NOT unitNameTable- maybe this should change).
+vars.ref - vec3 ref point, maybe overload for vec2 as well?
+vars.alt - boolean, if used, includes altitude in string
+vars.metric - boolean, gives distance in km instead of NM.
+]]
+function mist.getBRString(vars)
+ local units = vars.units
+ local ref = mist.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already.
+ local alt = vars.alt
+ local metric = vars.metric
+ local avgPos = mist.getAvgPos(units)
+ if avgPos then
+ local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z}
+ local dir = mist.utils.getDir(vec, ref)
+ local dist = mist.utils.get2DDist(avgPos, ref)
+ if alt then
+ alt = avgPos.y
+ end
+ return mist.tostringBR(dir, dist, alt, metric)
+ end
+end
+
+-- Returns the Vec3 coordinates of the average position of the concentration of units most in the heading direction.
+--[[ vars for mist.getLeadingPos:
+vars.units - table of unit names
+vars.heading - direction
+vars.radius - number
+vars.headingDegrees - boolean, switches heading to degrees
+]]
+function mist.getLeadingPos(vars)
+ local units = vars.units
+ local heading = vars.heading
+ local radius = vars.radius
+ if vars.headingDegrees then
+ heading = mist.utils.toRadian(vars.headingDegrees)
+ end
+
+ local unitPosTbl = {}
+ for i = 1, #units do
+ local unit = Unit.getByName(units[i])
+ if unit and unit:isExist() then
+ unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p
+ end
+ end
+ if #unitPosTbl > 0 then -- one more more units found.
+ -- first, find the unit most in the heading direction
+ local maxPos = -math.huge
+
+ local maxPosInd -- maxPos - the furthest in direction defined by heading; maxPosInd =
+ for i = 1, #unitPosTbl do
+ local rotatedVec2 = mist.vec.rotateVec2(mist.utils.makeVec2(unitPosTbl[i]), heading)
+ if (not maxPos) or maxPos < rotatedVec2.x then
+ maxPos = rotatedVec2.x
+ maxPosInd = i
+ end
+ end
+
+ --now, get all the units around this unit...
+ local avgPos
+ if radius then
+ local maxUnitPos = unitPosTbl[maxPosInd]
+ local avgx, avgy, avgz, totNum = 0, 0, 0, 0
+ for i = 1, #unitPosTbl do
+ if mist.utils.get2DDist(maxUnitPos, unitPosTbl[i]) <= radius then
+ avgx = avgx + unitPosTbl[i].x
+ avgy = avgy + unitPosTbl[i].y
+ avgz = avgz + unitPosTbl[i].z
+ totNum = totNum + 1
+ end
+ end
+ avgPos = { x = avgx/totNum, y = avgy/totNum, z = avgz/totNum}
+ else
+ avgPos = unitPosTbl[maxPosInd]
+ end
+
+ return avgPos
+ end
+end
+
+--[[ vars for mist.getLeadingMGRSString:
+vars.units - table of unit names
+vars.heading - direction
+vars.radius - number
+vars.headingDegrees - boolean, switches heading to degrees
+vars.acc - number, 0 to 5.
+]]
+function mist.getLeadingMGRSString(vars)
+ local pos = mist.getLeadingPos(vars)
+ if pos then
+ local acc = vars.acc or 5
+ return mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), acc)
+ end
+end
+
+--[[ vars for mist.getLeadingLLString:
+vars.units - table of unit names
+vars.heading - direction, number
+vars.radius - number
+vars.headingDegrees - boolean, switches heading to degrees
+vars.acc - number of digits after decimal point (can be negative)
+vars.DMS - boolean, true if you want DMS.
+]]
+function mist.getLeadingLLString(vars)
+ local pos = mist.getLeadingPos(vars)
+ if pos then
+ local acc = vars.acc or 3
+ local DMS = vars.DMS
+ local lat, lon = coord.LOtoLL(pos)
+ return mist.tostringLL(lat, lon, acc, DMS)
+ end
+end
+
+--[[ vars for mist.getLeadingBRString:
+vars.units - table of unit names
+vars.heading - direction, number
+vars.radius - number
+vars.headingDegrees - boolean, switches heading to degrees
+vars.metric - boolean, if true, use km instead of NM.
+vars.alt - boolean, if true, include altitude.
+vars.ref - vec3/vec2 reference point.
+]]
+function mist.getLeadingBRString(vars)
+ local pos = mist.getLeadingPos(vars)
+ if pos then
+ local ref = vars.ref
+ local alt = vars.alt
+ local metric = vars.metric
+
+ local vec = {x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z}
+ local dir = mist.utils.getDir(vec, ref)
+ local dist = mist.utils.get2DDist(pos, ref)
+ if alt then
+ alt = pos.y
+ end
+ return mist.tostringBR(dir, dist, alt, metric)
+ end
+end
+
+end
+
+--- Group functions.
+-- @section groups
+do -- group functions scope
+
+ --- Check table used for group creation.
+ -- @tparam table groupData table to check.
+ -- @treturn boolean true if a group can be spawned using
+ -- this table, false otherwise.
+ function mist.groupTableCheck(groupData)
+ -- return false if country, category
+ -- or units are missing
+ if not groupData.country or
+ not groupData.category or
+ not groupData.units then
+ return false
+ end
+ -- return false if unitData misses
+ -- x, y or type
+ for unitId, unitData in pairs(groupData.units) do
+ if not unitData.x or
+ not unitData.y or
+ not unitData.type then
+ return false
+ end
+ end
+ -- everything we need is here return true
+ return true
+ end
+
+ --- Returns group data table of give group.
+ function mist.getCurrentGroupData(gpName)
+ local dbData = mist.getGroupData(gpName)
+
+ if Group.getByName(gpName) and Group.getByName(gpName):isExist() == true then
+ local newGroup = Group.getByName(gpName)
+ local newData = {}
+ newData.name = gpName
+ newData.groupId = tonumber(newGroup:getID())
+ newData.category = newGroup:getCategory()
+ newData.groupName = gpName
+ newData.hidden = dbData.hidden
+
+ if newData.category == 2 then
+ newData.category = 'vehicle'
+ elseif newData.category == 3 then
+ newData.category = 'ship'
+ end
+
+ newData.units = {}
+ local newUnits = newGroup:getUnits()
+ for unitNum, unitData in pairs(newGroup:getUnits()) do
+ newData.units[unitNum] = {}
+ local uName = unitData:getName()
+
+ if mist.DBs.unitsByName[uName] and unitData:getTypeName() == mist.DBs.unitsByName[uName].type and mist.DBs.unitsByName[uName].unitId == tonumber(unitData:getID()) then -- If old data matches most of new data
+ newData.units[unitNum] = mist.utils.deepCopy(mist.DBs.unitsByName[uName])
+ else
+ newData.units[unitNum].unitId = tonumber(unitData:getID())
+ newData.units[unitNum].type = unitData:getTypeName()
+ newData.units[unitNum].skill = mist.getUnitSkill(uName)
+ newData.country = string.lower(country.name[unitData:getCountry()])
+ newData.units[unitNum].callsign = unitData:getCallsign()
+ newData.units[unitNum].unitName = uName
+ end
+
+ newData.units[unitNum].x = unitData:getPosition().p.x
+ newData.units[unitNum].y = unitData:getPosition().p.z
+ newData.units[unitNum].point = {x = newData.units[unitNum].x, y = newData.units[unitNum].y}
+ newData.units[unitNum].heading = mist.getHeading(unitData, true) -- added to DBs
+ newData.units[unitNum].alt = unitData:getPosition().p.y
+ newData.units[unitNum].speed = mist.vec.mag(unitData:getVelocity())
+
+ end
+
+ return newData
+ elseif StaticObject.getByName(gpName) and StaticObject.getByName(gpName):isExist() == true then
+ local staticObj = StaticObject.getByName(gpName)
+ dbData.units[1].x = staticObj:getPosition().p.x
+ dbData.units[1].y = staticObj:getPosition().p.z
+ dbData.units[1].alt = staticObj:getPosition().p.y
+ dbData.units[1].heading = mist.getHeading(staticObj, true)
+
+ return dbData
+ end
+
+ end
+
+ function mist.getGroupData(gpName)
+ local found = false
+ local newData = {}
+ if mist.DBs.groupsByName[gpName] then
+ newData = mist.utils.deepCopy(mist.DBs.groupsByName[gpName])
+ found = true
+ end
+
+ if found == false then
+ for groupName, groupData in pairs(mist.DBs.groupsByName) do
+ if mist.stringMatch(groupName, gpName) == true then
+ newData = mist.utils.deepCopy(groupData)
+ newData.groupName = groupName
+ found = true
+ break
+ end
+ end
+ end
+
+ local payloads
+ if newData.category == 'plane' or newData.category == 'helicopter' then
+ payloads = mist.getGroupPayload(newData.groupName)
+ end
+ if found == true then
+ --newData.hidden = false -- maybe add this to DBs
+
+ for unitNum, unitData in pairs(newData.units) do
+ newData.units[unitNum] = {}
+
+ newData.units[unitNum].unitId = unitData.unitId
+ --newData.units[unitNum].point = unitData.point
+ newData.units[unitNum].x = unitData.point.x
+ newData.units[unitNum].y = unitData.point.y
+ newData.units[unitNum].alt = unitData.alt
+ newData.units[unitNum].alt_type = unitData.alt_type
+ newData.units[unitNum].speed = unitData.speed
+ newData.units[unitNum].type = unitData.type
+ newData.units[unitNum].skill = unitData.skill
+ newData.units[unitNum].unitName = unitData.unitName
+ newData.units[unitNum].heading = unitData.heading -- added to DBs
+ newData.units[unitNum].playerCanDrive = unitData.playerCanDrive -- added to DBs
+
+
+ if newData.category == 'plane' or newData.category == 'helicopter' then
+ newData.units[unitNum].payload = payloads[unitNum]
+ newData.units[unitNum].livery_id = unitData.livery_id
+ newData.units[unitNum].onboard_num = unitData.onboard_num
+ newData.units[unitNum].callsign = unitData.callsign
+ newData.units[unitNum].AddPropAircraft = unitData.AddPropAircraft
+ end
+ if newData.category == 'static' then
+ newData.units[unitNum].categoryStatic = unitData.categoryStatic
+ newData.units[unitNum].mass = unitData.mass
+ newData.units[unitNum].canCargo = unitData.canCargo
+ newData.units[unitNum].shape_name = unitData.shape_name
+ end
+ end
+ --log:info(newData)
+ return newData
+ else
+ log:error('$1 not found in MIST database', gpName)
+ return
+ end
+ end
+
+ function mist.getPayload(unitIdent)
+ -- refactor to search by groupId and allow groupId and groupName as inputs
+ local unitId = unitIdent
+ if type(unitIdent) == 'string' and not tonumber(unitIdent) then
+ if mist.DBs.MEunitsByName[unitIdent] then
+ unitId = mist.DBs.MEunitsByName[unitIdent].unitId
+ else
+ log:error("Unit not found in mist.DBs.MEunitsByName: $1", unitIdent)
+ end
+ end
+ local gpId = mist.DBs.MEunitsById[unitId].groupId
+
+ if gpId and unitId then
+ for coa_name, coa_data in pairs(env.mission.coalition) do
+ if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then
+ if coa_data.country then --there is a country table
+ for cntry_id, cntry_data in pairs(coa_data.country) do
+ for obj_type_name, obj_type_data in pairs(cntry_data) do
+ if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points
+ if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group!
+ for group_num, group_data in pairs(obj_type_data.group) do
+ if group_data and group_data.groupId == gpId then
+ for unitIndex, unitData in pairs(group_data.units) do --group index
+ if unitData.unitId == unitId then
+ return unitData.payload
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ else
+ log:error('Need string or number. Got: $1', type(unitIdent))
+ return false
+ end
+ log:warn("Couldn't find payload for unit: $1", unitIdent)
+ return
+ end
+
+ function mist.getGroupPayload(groupIdent)
+ local gpId = groupIdent
+ if type(groupIdent) == 'string' and not tonumber(groupIdent) then
+ if mist.DBs.MEgroupsByName[groupIdent] then
+ gpId = mist.DBs.MEgroupsByName[groupIdent].groupId
+ else
+ log:error('$1 not found in mist.DBs.MEgroupsByName', groupIdent)
+ end
+ end
+
+ if gpId then
+ for coa_name, coa_data in pairs(env.mission.coalition) do
+ if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then
+ if coa_data.country then --there is a country table
+ for cntry_id, cntry_data in pairs(coa_data.country) do
+ for obj_type_name, obj_type_data in pairs(cntry_data) do
+ if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points
+ if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group!
+ for group_num, group_data in pairs(obj_type_data.group) do
+ if group_data and group_data.groupId == gpId then
+ local payloads = {}
+ for unitIndex, unitData in pairs(group_data.units) do --group index
+ payloads[unitIndex] = unitData.payload
+ end
+ return payloads
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ else
+ log:error('Need string or number. Got: $1', type(groupIdent))
+ return false
+ end
+ log:warn("Couldn't find payload for group: $1", groupIdent)
+ return
+
+ end
+
+ function mist.teleportToPoint(vars) -- main teleport function that all of teleport/respawn functions call
+ local point = vars.point
+
+ local gpName
+ if vars.gpName then
+ gpName = vars.gpName
+ elseif vars.groupName then
+ gpName = vars.groupName
+ else
+ log:error('Missing field groupName or gpName in variable table')
+ end
+
+ local action = vars.action
+
+ local disperse = vars.disperse or false
+ local maxDisp = vars.maxDisp
+ if not vars.maxDisp then
+ maxDisp = 200
+ else
+ maxDisp = vars.maxDisp
+ end
+ local radius = vars.radius or 0
+ local innerRadius = vars.innerRadius
+
+ local route = vars.route
+ local dbData = false
+
+ local newGroupData
+ if gpName and not vars.groupData then
+ if string.lower(action) == 'teleport' or string.lower(action) == 'tele' then
+ newGroupData = mist.getCurrentGroupData(gpName)
+ elseif string.lower(action) == 'respawn' then
+ newGroupData = mist.getGroupData(gpName)
+ dbData = true
+ elseif string.lower(action) == 'clone' then
+ newGroupData = mist.getGroupData(gpName)
+ newGroupData.clone = 'order66'
+ dbData = true
+ else
+ action = 'tele'
+ newGroupData = mist.getCurrentGroupData(gpName)
+ end
+ else
+ action = 'tele'
+ newGroupData = vars.groupData
+ end
+
+ --log:info('get Randomized Point')
+ local diff = {x = 0, y = 0}
+ local newCoord, origCoord
+ if point then
+ local valid = false
+
+ local validTerrain
+ if string.lower(newGroupData.category) == 'ship' then
+ validTerrain = {'SHALLOW_WATER' , 'WATER'}
+ elseif string.lower(newGroupData.category) == 'vehicle' then
+ validTerrain = {'LAND', 'ROAD'}
+ else
+ validTerrain = {'LAND', 'ROAD', 'SHALLOW_WATER', 'WATER', 'RUNWAY'}
+ end
+
+ for i = 1, 100 do
+ newCoord = mist.getRandPointInCircle(point, radius, innerRadius)
+ if mist.isTerrainValid(newCoord, validTerrain) then
+ origCoord = mist.utils.deepCopy(newCoord)
+ diff = {x = (newCoord.x - newGroupData.units[1].x), y = (newCoord.y - newGroupData.units[1].y)}
+ valid = true
+ break
+ end
+ end
+ if valid == false then
+ log:error('Point supplied in variable table is not a valid coordinate. Valid coords: $1', validTerrain)
+ return false
+ end
+ end
+ if not newGroupData.country and mist.DBs.groupsByName[newGroupData.groupName].country then
+ newGroupData.country = mist.DBs.groupsByName[newGroupData.groupName].country
+ end
+ if not newGroupData.category and mist.DBs.groupsByName[newGroupData.groupName].category then
+ newGroupData.category = mist.DBs.groupsByName[newGroupData.groupName].category
+ end
+
+ for unitNum, unitData in pairs(newGroupData.units) do
+ if disperse then
+ if maxDisp and type(maxDisp) == 'number' and unitNum ~= 1 then
+ newCoord = mist.getRandPointInCircle(origCoord, maxDisp)
+ --else
+ --newCoord = mist.getRandPointInCircle(zone.point, zone.radius)
+ end
+
+ newGroupData.units[unitNum].x = newCoord.x
+ newGroupData.units[unitNum].y = newCoord.y
+ else
+ newGroupData.units[unitNum].x = unitData.x + diff.x
+ newGroupData.units[unitNum].y = unitData.y + diff.y
+ end
+ if point then
+ if (newGroupData.category == 'plane' or newGroupData.category == 'helicopter') then
+ if point.z and point.y > 0 and point.y > land.getHeight({newGroupData.units[unitNum].x, newGroupData.units[unitNum].y}) + 10 then
+ newGroupData.units[unitNum].alt = point.y
+ else
+ if newGroupData.category == 'plane' then
+ newGroupData.units[unitNum].alt = land.getHeight({newGroupData.units[unitNum].x, newGroupData.units[unitNum].y}) + math.random(300, 9000)
+ else
+ newGroupData.units[unitNum].alt = land.getHeight({newGroupData.units[unitNum].x, newGroupData.units[unitNum].y}) + math.random(200, 3000)
+ end
+ end
+ end
+ end
+ end
+
+ if newGroupData.start_time then
+ newGroupData.startTime = newGroupData.start_time
+ end
+
+ if newGroupData.startTime and newGroupData.startTime ~= 0 and dbData == true then
+ local timeDif = timer.getAbsTime() - timer.getTime0()
+ if timeDif > newGroupData.startTime then
+ newGroupData.startTime = 0
+ else
+ newGroupData.startTime = newGroupData.startTime - timeDif
+ end
+
+ end
+
+ if route then
+ newGroupData.route = route
+ end
+ --mist.debug.writeData(mist.utils.serialize,{'teleportToPoint', newGroupData}, 'newGroupData.lua')
+ if string.lower(newGroupData.category) == 'static' then
+ --log:info(newGroupData)
+ return mist.dynAddStatic(newGroupData)
+ end
+ return mist.dynAdd(newGroupData)
+
+ end
+
+ function mist.respawnInZone(gpName, zone, disperse, maxDisp)
+
+ if type(gpName) == 'table' and gpName:getName() then
+ gpName = gpName:getName()
+ elseif type(gpName) == 'table' and gpName[1]:getName() then
+ gpName = math.random(#gpName)
+ else
+ gpName = tostring(gpName)
+ end
+
+ if type(zone) == 'string' then
+ zone = trigger.misc.getZone(zone)
+ elseif type(zone) == 'table' and not zone.radius then
+ zone = trigger.misc.getZone(zone[math.random(1, #zone)])
+ end
+ local vars = {}
+ vars.gpName = gpName
+ vars.action = 'respawn'
+ vars.point = zone.point
+ vars.radius = zone.radius
+ vars.disperse = disperse
+ vars.maxDisp = maxDisp
+ return mist.teleportToPoint(vars)
+ end
+
+ function mist.cloneInZone(gpName, zone, disperse, maxDisp)
+ --log:info('cloneInZone')
+ if type(gpName) == 'table' then
+ gpName = gpName:getName()
+ else
+ gpName = tostring(gpName)
+ end
+
+ if type(zone) == 'string' then
+ zone = trigger.misc.getZone(zone)
+ elseif type(zone) == 'table' and not zone.radius then
+ zone = trigger.misc.getZone(zone[math.random(1, #zone)])
+ end
+ local vars = {}
+ vars.gpName = gpName
+ vars.action = 'clone'
+ vars.point = zone.point
+ vars.radius = zone.radius
+ vars.disperse = disperse
+ vars.maxDisp = maxDisp
+ --log:info('do teleport')
+ return mist.teleportToPoint(vars)
+ end
+
+ function mist.teleportInZone(gpName, zone, disperse, maxDisp) -- groupName, zoneName or table of Zone Names, keepForm is a boolean
+ if type(gpName) == 'table' and gpName:getName() then
+ gpName = gpName:getName()
+ else
+ gpName = tostring(gpName)
+ end
+
+ if type(zone) == 'string' then
+ zone = trigger.misc.getZone(zone)
+ elseif type(zone) == 'table' and not zone.radius then
+ zone = trigger.misc.getZone(zone[math.random(1, #zone)])
+ end
+
+ local vars = {}
+ vars.gpName = gpName
+ vars.action = 'tele'
+ vars.point = zone.point
+ vars.radius = zone.radius
+ vars.disperse = disperse
+ vars.maxDisp = maxDisp
+ return mist.teleportToPoint(vars)
+ end
+
+ function mist.respawnGroup(gpName, task)
+ local vars = {}
+ vars.gpName = gpName
+ vars.action = 'respawn'
+ if task and type(task) ~= 'number' then
+ vars.route = mist.getGroupRoute(gpName, 'task')
+ end
+ local newGroup = mist.teleportToPoint(vars)
+ if task and type(task) == 'number' then
+ local newRoute = mist.getGroupRoute(gpName, 'task')
+ mist.scheduleFunction(mist.goRoute, {newGroup, newRoute}, timer.getTime() + task)
+ end
+ return newGroup
+ end
+
+ function mist.cloneGroup(gpName, task)
+ local vars = {}
+ vars.gpName = gpName
+ vars.action = 'clone'
+ if task and type(task) ~= 'number' then
+ vars.route = mist.getGroupRoute(gpName, 'task')
+ end
+ local newGroup = mist.teleportToPoint(vars)
+ if task and type(task) == 'number' then
+ local newRoute = mist.getGroupRoute(gpName, 'task')
+ mist.scheduleFunction(mist.goRoute, {newGroup, newRoute}, timer.getTime() + task)
+ end
+ return newGroup
+ end
+
+ function mist.teleportGroup(gpName, task)
+ local vars = {}
+ vars.gpName = gpName
+ vars.action = 'teleport'
+ if task and type(task) ~= 'number' then
+ vars.route = mist.getGroupRoute(gpName, 'task')
+ end
+ local newGroup = mist.teleportToPoint(vars)
+ if task and type(task) == 'number' then
+ local newRoute = mist.getGroupRoute(gpName, 'task')
+ mist.scheduleFunction(mist.goRoute, {newGroup, newRoute}, timer.getTime() + task)
+ end
+ return newGroup
+ end
+
+ function mist.spawnRandomizedGroup(groupName, vars) -- need to debug
+ if Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then
+ local gpData = mist.getGroupData(groupName)
+ gpData.units = mist.randomizeGroupOrder(gpData.units, vars)
+ gpData.route = mist.getGroupRoute(groupName, 'task')
+
+ mist.dynAdd(gpData)
+ end
+
+ return true
+ end
+
+ function mist.randomizeNumTable(vars)
+ local newTable = {}
+
+ local excludeIndex = {}
+ local randomTable = {}
+
+ if vars and vars.exclude and type(vars.exclude) == 'table' then
+ for index, data in pairs(vars.exclude) do
+ excludeIndex[data] = true
+ end
+ end
+
+ local low, hi, size
+
+ if vars.size then
+ size = vars.size
+ end
+
+ if vars and vars.lowerLimit and type(vars.lowerLimit) == 'number' then
+ low = mist.utils.round(vars.lowerLimit)
+ else
+ low = 1
+ end
+
+ if vars and vars.upperLimit and type(vars.upperLimit) == 'number' then
+ hi = mist.utils.round(vars.upperLimit)
+ else
+ hi = size
+ end
+
+ local choices = {}
+ -- add to exclude list and create list of what to randomize
+ for i = 1, size do
+ if not (i >= low and i <= hi) then
+
+ excludeIndex[i] = true
+ end
+ if not excludeIndex[i] then
+ table.insert(choices, i)
+ else
+ newTable[i] = i
+ end
+ end
+
+ for ind, num in pairs(choices) do
+ local found = false
+ local x = 0
+ while found == false do
+ x = mist.random(size) -- get random number from list
+ local addNew = true
+ for index, _ in pairs(excludeIndex) do
+ if index == x then
+ addNew = false
+ break
+ end
+ end
+ if addNew == true then
+ excludeIndex[x] = true
+ found = true
+ end
+ excludeIndex[x] = true
+
+ end
+ newTable[num] = x
+ end
+ --[[
+ for i = 1, #newTable do
+ log:info(newTable[i])
+ end
+ ]]
+ return newTable
+ end
+
+ function mist.randomizeGroupOrder(passedUnits, vars)
+ -- figure out what to exclude, and send data to other func
+ local units = passedUnits
+
+ if passedUnits.units then
+ units = passUnits.units
+ end
+
+ local exclude = {}
+ local excludeNum = {}
+ if vars and vars.excludeType and type(vars.excludeType) == 'table' then
+ exclude = vars.excludeType
+ end
+
+ if vars and vars.excludeNum and type(vars.excludeNum) == 'table' then
+ excludeNum = vars.excludeNum
+ end
+
+ local low, hi
+
+ if vars and vars.lowerLimit and type(vars.lowerLimit) == 'number' then
+ low = mist.utils.round(vars.lowerLimit)
+ else
+ low = 1
+ end
+
+ if vars and vars.upperLimit and type(vars.upperLimit) == 'number' then
+ hi = mist.utils.round(vars.upperLimit)
+ else
+ hi = #units
+ end
+
+
+ local excludeNum = {}
+ for unitIndex, unitData in pairs(units) do
+ if unitIndex >= low and unitIndex <= hi then -- if within range
+ local found = false
+ if #exclude > 0 then
+ for excludeType, index in pairs(exclude) do -- check if excluded
+ if mist.stringMatch(excludeType, unitData.type) then -- if excluded
+ excludeNum[unitIndex] = unitIndex
+ found = true
+ end
+ end
+ end
+ else -- unitIndex is either to low, or to high: added to exclude list
+ excludeNum[unitIndex] = unitId
+ end
+ end
+
+ local newGroup = {}
+ local newOrder = mist.randomizeNumTable({exclude = excludeNum, size = #units})
+
+ for unitIndex, unitData in pairs(units) do
+ for i = 1, #newOrder do
+ if newOrder[i] == unitIndex then
+ newGroup[i] = mist.utils.deepCopy(units[i]) -- gets all of the unit data
+ newGroup[i].type = mist.utils.deepCopy(unitData.type)
+ newGroup[i].skill = mist.utils.deepCopy(unitData.skill)
+ newGroup[i].unitName = mist.utils.deepCopy(unitData.unitName)
+ newGroup[i].unitIndex = mist.utils.deepCopy(unitData.unitIndex) -- replaces the units data with a new type
+ end
+ end
+ end
+ return newGroup
+ end
+
+ function mist.random(firstNum, secondNum) -- no support for decimals
+ local lowNum, highNum
+ if not secondNum then
+ highNum = firstNum
+ lowNum = 1
+ else
+ lowNum = firstNum
+ highNum = secondNum
+ end
+ local total = 1
+ if math.abs(highNum - lowNum + 1) < 50 then -- if total values is less than 50
+ total = math.modf(50/math.abs(highNum - lowNum + 1)) -- make x copies required to be above 50
+ end
+ local choices = {}
+ for i = 1, total do -- iterate required number of times
+ for x = lowNum, highNum do -- iterate between the range
+ choices[#choices +1] = x -- add each entry to a table
+ end
+ end
+ local rtnVal = math.random(#choices) -- will now do a math.random of at least 50 choices
+ for i = 1, 10 do
+ rtnVal = math.random(#choices) -- iterate a few times for giggles
+ end
+ return choices[rtnVal]
+ end
+
+ function mist.stringMatch(s1, s2, bool)
+ local exclude = {'%-', '%(', '%)', '%_', '%[', '%]', '%.', '%#', '% ', '%{', '%}', '%$', '%%', '%?', '%+', '%^'}
+ if type(s1) == 'string' and type(s2) == 'string' then
+ for i , str in pairs(exclude) do
+ s1 = string.gsub(s1, str, '')
+ s2 = string.gsub(s2, str, '')
+ end
+ if not bool then
+ s1 = string.lower(s1)
+ s2 = string.lower(s2)
+ end
+ log:info('Comparing: $1 and $2', s1, s2)
+ if s1 == s2 then
+ return true
+ else
+ return false
+ end
+ else
+ log:error('Either the first or second variable were not a string')
+ return false
+ end
+ end
+
+ mist.matchString = mist.stringMatch -- both commands work because order out type of I
+
+ --[[ scope:
+{
+ units = {...}, -- unit names.
+ coa = {...}, -- coa names
+ countries = {...}, -- country names
+ CA = {...}, -- looks just like coa.
+ unitTypes = { red = {}, blue = {}, all = {}, Russia = {},}
+}
+
+
+scope examples:
+
+{ units = { 'Hawg11', 'Hawg12' }, CA = {'blue'} }
+
+{ countries = {'Georgia'}, unitTypes = {blue = {'A-10C', 'A-10A'}}}
+
+{ coa = {'all'}}
+
+{unitTypes = { blue = {'A-10C'}}}
+]]
+end
+
+--- Utility functions.
+-- E.g. conversions between units etc.
+-- @section mist.utils
+do -- mist.util scope
+ mist.utils = {}
+
+ --- Converts angle in radians to degrees.
+ -- @param angle angle in radians
+ -- @return angle in degrees
+ function mist.utils.toDegree(angle)
+ return angle*180/math.pi
+ end
+
+ --- Converts angle in degrees to radians.
+ -- @param angle angle in degrees
+ -- @return angle in degrees
+ function mist.utils.toRadian(angle)
+ return angle*math.pi/180
+ end
+
+ --- Converts meters to nautical miles.
+ -- @param meters distance in meters
+ -- @return distance in nautical miles
+ function mist.utils.metersToNM(meters)
+ return meters/1852
+ end
+
+ --- Converts meters to feet.
+ -- @param meters distance in meters
+ -- @return distance in feet
+ function mist.utils.metersToFeet(meters)
+ return meters/0.3048
+ end
+
+ --- Converts nautical miles to meters.
+ -- @param nm distance in nautical miles
+ -- @return distance in meters
+ function mist.utils.NMToMeters(nm)
+ return nm*1852
+ end
+
+ --- Converts feet to meters.
+ -- @param feet distance in feet
+ -- @return distance in meters
+ function mist.utils.feetToMeters(feet)
+ return feet*0.3048
+ end
+
+ --- Converts meters per second to knots.
+ -- @param mps speed in m/s
+ -- @return speed in knots
+ function mist.utils.mpsToKnots(mps)
+ return mps*3600/1852
+ end
+
+ --- Converts meters per second to kilometers per hour.
+ -- @param mps speed in m/s
+ -- @return speed in km/h
+ function mist.utils.mpsToKmph(mps)
+ return mps*3.6
+ end
+
+ --- Converts knots to meters per second.
+ -- @param knots speed in knots
+ -- @return speed in m/s
+ function mist.utils.knotsToMps(knots)
+ return knots*1852/3600
+ end
+
+ --- Converts kilometers per hour to meters per second.
+ -- @param kmph speed in km/h
+ -- @return speed in m/s
+ function mist.utils.kmphToMps(kmph)
+ return kmph/3.6
+ end
+
+ --- Converts a Vec3 to a Vec2.
+ -- @tparam Vec3 vec the 3D vector
+ -- @return vector converted to Vec2
+ function mist.utils.makeVec2(vec)
+ if vec.z then
+ return {x = vec.x, y = vec.z}
+ else
+ return {x = vec.x, y = vec.y} -- it was actually already vec2.
+ end
+ end
+
+ --- Converts a Vec2 to a Vec3.
+ -- @tparam Vec2 vec the 2D vector
+ -- @param y optional new y axis (altitude) value. If omitted it's 0.
+ function mist.utils.makeVec3(vec, y)
+ if not vec.z then
+ if vec.alt and not y then
+ y = vec.alt
+ elseif not y then
+ y = 0
+ end
+ return {x = vec.x, y = y, z = vec.y}
+ else
+ return {x = vec.x, y = vec.y, z = vec.z} -- it was already Vec3, actually.
+ end
+ end
+
+ --- Converts a Vec2 to a Vec3 using ground level as altitude.
+ -- The ground level at the specific point is used as altitude (y-axis)
+ -- for the new vector. Optionally a offset can be specified.
+ -- @tparam Vec2 vec the 2D vector
+ -- @param[opt] offset offset to be applied to the ground level
+ -- @return new 3D vector
+ function mist.utils.makeVec3GL(vec, offset)
+ local adj = offset or 0
+
+ if not vec.z then
+ return {x = vec.x, y = (land.getHeight(vec) + adj), z = vec.y}
+ else
+ return {x = vec.x, y = (land.getHeight({x = vec.x, y = vec.z}) + adj), z = vec.z}
+ end
+ end
+
+ --- Returns the center of a zone as Vec3.
+ -- @tparam string|table zone trigger zone name or table
+ -- @treturn Vec3 center of the zone
+ function mist.utils.zoneToVec3(zone)
+ local new = {}
+ if type(zone) == 'table' then
+ if zone.point then
+ new.x = zone.point.x
+ new.y = zone.point.y
+ new.z = zone.point.z
+ elseif zone.x and zone.y and zone.z then
+ return zone
+ end
+ return new
+ elseif type(zone) == 'string' then
+ zone = trigger.misc.getZone(zone)
+ if zone then
+ new.x = zone.point.x
+ new.y = zone.point.y
+ new.z = zone.point.z
+ return new
+ end
+ end
+ end
+
+ --- Returns heading-error corrected direction.
+ -- True-north corrected direction from point along vector vec.
+ -- @tparam Vec3 vec
+ -- @tparam Vec2 point
+ -- @return heading-error corrected direction from point.
+ function mist.utils.getDir(vec, point)
+ local dir = math.atan2(vec.z, vec.x)
+ if point then
+ dir = dir + mist.getNorthCorrection(point)
+ end
+ if dir < 0 then
+ dir = dir + 2 * math.pi -- put dir in range of 0 to 2*pi
+ end
+ return dir
+ end
+
+ --- Returns distance in meters between two points.
+ -- @tparam Vec2|Vec3 point1 first point
+ -- @tparam Vec2|Vec3 point2 second point
+ -- @treturn number distance between given points.
+ function mist.utils.get2DDist(point1, point2)
+ point1 = mist.utils.makeVec3(point1)
+ point2 = mist.utils.makeVec3(point2)
+ return mist.vec.mag({x = point1.x - point2.x, y = 0, z = point1.z - point2.z})
+ end
+
+ --- Returns distance in meters between two points in 3D space.
+ -- @tparam Vec3 point1 first point
+ -- @tparam Vec3 point2 second point
+ -- @treturn number distancen between given points in 3D space.
+ function mist.utils.get3DDist(point1, point2)
+ return mist.vec.mag({x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z})
+ end
+
+ --- Creates a waypoint from a vector.
+ -- @tparam Vec2|Vec3 vec position of the new waypoint
+ -- @treturn Waypoint a new waypoint to be used inside paths.
+ function mist.utils.vecToWP(vec)
+ local newWP = {}
+ newWP.x = vec.x
+ newWP.y = vec.y
+ if vec.z then
+ newWP.alt = vec.y
+ newWP.y = vec.z
+ else
+ newWP.alt = land.getHeight({x = vec.x, y = vec.y})
+ end
+ return newWP
+ end
+
+ --- Creates a waypoint from a unit.
+ -- This function also considers the units speed.
+ -- The alt_type of this waypoint is set to "BARO".
+ -- @tparam Unit pUnit Unit whose position and speed will be used.
+ -- @treturn Waypoint new waypoint.
+ function mist.utils.unitToWP(pUnit)
+ local unit = mist.utils.deepCopy(pUnit)
+ if type(unit) == 'string' then
+ if Unit.getByName(unit) then
+ unit = Unit.getByName(unit)
+ end
+ end
+ if unit:isExist() == true then
+ local new = mist.utils.vecToWP(unit:getPosition().p)
+ new.speed = mist.vec.mag(unit:getVelocity())
+ new.alt_type = "BARO"
+
+ return new
+ end
+ log:error("$1 not found or doesn't exist", pUnit)
+ return false
+ end
+
+ --- Creates a deep copy of a object.
+ -- Usually this object is a table.
+ -- See also: from http://lua-users.org/wiki/CopyTable
+ -- @param object object to copy
+ -- @return copy of object
+ function mist.utils.deepCopy(object)
+ local lookup_table = {}
+ local function _copy(object)
+ if type(object) ~= "table" then
+ return object
+ elseif lookup_table[object] then
+ return lookup_table[object]
+ end
+ local new_table = {}
+ lookup_table[object] = new_table
+ for index, value in pairs(object) do
+ new_table[_copy(index)] = _copy(value)
+ end
+ return setmetatable(new_table, getmetatable(object))
+ end
+ return _copy(object)
+ end
+
+ --- Simple rounding function.
+ -- From http://lua-users.org/wiki/SimpleRound
+ -- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place
+ -- @tparam number num number to round
+ -- @param idp
+ function mist.utils.round(num, idp)
+ local mult = 10^(idp or 0)
+ return math.floor(num * mult + 0.5) / mult
+ end
+
+ --- Rounds all numbers inside a table.
+ -- @tparam table tbl table in which to round numbers
+ -- @param idp
+ function mist.utils.roundTbl(tbl, idp)
+ for id, val in pairs(tbl) do
+ if type(val) == 'number' then
+ tbl[id] = mist.utils.round(val, idp)
+ end
+ end
+ return tbl
+ end
+
+ --- Executes the given string.
+ -- borrowed from Slmod
+ -- @tparam string s string containing LUA code.
+ -- @treturn boolean true if successfully executed, false otherwise
+ function mist.utils.dostring(s)
+ local f, err = loadstring(s)
+ if f then
+ return true, f()
+ else
+ return false, err
+ end
+ end
+
+ --- Checks a table's types.
+ -- This function checks a tables types against a specifically forged type table.
+ -- @param fname
+ -- @tparam table type_tbl
+ -- @tparam table var_tbl
+ -- @usage -- specifically forged type table
+ -- type_tbl = {
+ -- {'table', 'number'},
+ -- 'string',
+ -- 'number',
+ -- 'number',
+ -- {'string','nil'},
+ -- {'number', 'nil'}
+ -- }
+ -- -- my_tbl index 1 must be a table or a number;
+ -- -- index 2, a string; index 3, a number;
+ -- -- index 4, a number; index 5, either a string or nil;
+ -- -- and index 6, either a number or nil.
+ -- mist.utils.typeCheck(type_tbl, my_tb)
+ -- @return true if table passes the check, false otherwise.
+ function mist.utils.typeCheck(fname, type_tbl, var_tbl)
+ -- log:info('type check')
+ for type_key, type_val in pairs(type_tbl) do
+ -- log:info('type_key: $1 type_val: $2', type_key, type_val)
+
+ --type_key can be a table of accepted keys- so try to find one that is not nil
+ local type_key_str = ''
+ local act_key = type_key -- actual key within var_tbl - necessary to use for multiple possible key variables. Initialize to type_key
+ if type(type_key) == 'table' then
+
+ for i = 1, #type_key do
+ if i ~= 1 then
+ type_key_str = type_key_str .. '/'
+ end
+ type_key_str = type_key_str .. tostring(type_key[i])
+ if var_tbl[type_key[i]] ~= nil then
+ act_key = type_key[i] -- found a non-nil entry, make act_key now this val.
+ end
+ end
+ else
+ type_key_str = tostring(type_key)
+ end
+
+ local err_msg = 'Error in function ' .. fname .. ', parameter "' .. type_key_str .. '", expected: '
+ local passed_check = false
+
+ if type(type_tbl[type_key]) == 'table' then
+ -- log:info('err_msg, before: $1', err_msg)
+ for j = 1, #type_tbl[type_key] do
+
+ if j == 1 then
+ err_msg = err_msg .. type_tbl[type_key][j]
+ else
+ err_msg = err_msg .. ' or ' .. type_tbl[type_key][j]
+ end
+
+ if type(var_tbl[act_key]) == type_tbl[type_key][j] then
+ passed_check = true
+ end
+ end
+ -- log:info('err_msg, after: $1', err_msg)
+ else
+ -- log:info('err_msg, before: $1', err_msg)
+ err_msg = err_msg .. type_tbl[type_key]
+ -- log:info('err_msg, after: $1', err_msg)
+ if type(var_tbl[act_key]) == type_tbl[type_key] then
+ passed_check = true
+ end
+
+ end
+
+ if not passed_check then
+ err_msg = err_msg .. ', got ' .. type(var_tbl[act_key])
+ return false, err_msg
+ end
+ end
+ return true
+ end
+
+ --- Serializes the give variable to a string.
+ -- borrowed from slmod
+ -- @param var variable to serialize
+ -- @treturn string variable serialized to string
+ function mist.utils.basicSerialize(var)
+ if var == nil then
+ return "\"\""
+ else
+ if ((type(var) == 'number') or
+ (type(var) == 'boolean') or
+ (type(var) == 'function') or
+ (type(var) == 'table') or
+ (type(var) == 'userdata') ) then
+ return tostring(var)
+ elseif type(var) == 'string' then
+ var = string.format('%q', var)
+ return var
+ end
+ end
+end
+
+--- Serialize value
+-- borrowed from slmod (serialize_slmod)
+-- @param name
+-- @param value value to serialize
+-- @param level
+function mist.utils.serialize(name, value, level)
+ --Based on ED's serialize_simple2
+ local function basicSerialize(o)
+ if type(o) == "number" then
+ return tostring(o)
+ elseif type(o) == "boolean" then
+ return tostring(o)
+ else -- assume it is a string
+ return mist.utils.basicSerialize(o)
+ end
+ end
+
+ local function serializeToTbl(name, value, level)
+ local var_str_tbl = {}
+ if level == nil then level = "" end
+ if level ~= "" then level = level.." " end
+
+ table.insert(var_str_tbl, level .. name .. " = ")
+
+ if type(value) == "number" or type(value) == "string" or type(value) == "boolean" then
+ table.insert(var_str_tbl, basicSerialize(value) .. ",\n")
+ elseif type(value) == "table" then
+ table.insert(var_str_tbl, "\n"..level.."{\n")
+
+ for k,v in pairs(value) do -- serialize its fields
+ local key
+ if type(k) == "number" then
+ key = string.format("[%s]", k)
+ else
+ key = string.format("[%q]", k)
+ end
+
+ table.insert(var_str_tbl, mist.utils.serialize(key, v, level.." "))
+
+ end
+ if level == "" then
+ table.insert(var_str_tbl, level.."} -- end of "..name.."\n")
+
+ else
+ table.insert(var_str_tbl, level.."}, -- end of "..name.."\n")
+
+ end
+ else
+ log:error('Cannot serialize a $1', type(value))
+ end
+ return var_str_tbl
+ end
+
+ local t_str = serializeToTbl(name, value, level)
+
+ return table.concat(t_str)
+end
+
+--- Serialize value supporting cycles.
+-- borrowed from slmod (serialize_wcycles)
+-- @param name
+-- @param value value to serialize
+-- @param saved
+function mist.utils.serializeWithCycles(name, value, saved)
+ --mostly straight out of Programming in Lua
+ local function basicSerialize(o)
+ if type(o) == "number" then
+ return tostring(o)
+ elseif type(o) == "boolean" then
+ return tostring(o)
+ else -- assume it is a string
+ return mist.utils.basicSerialize(o)
+ end
+ end
+
+ local t_str = {}
+ saved = saved or {} -- initial value
+ if ((type(value) == 'string') or (type(value) == 'number') or (type(value) == 'table') or (type(value) == 'boolean')) then
+ table.insert(t_str, name .. " = ")
+ if type(value) == "number" or type(value) == "string" or type(value) == "boolean" then
+ table.insert(t_str, basicSerialize(value) .. "\n")
+ else
+
+ if saved[value] then -- value already saved?
+ table.insert(t_str, saved[value] .. "\n")
+ else
+ saved[value] = name -- save name for next time
+ table.insert(t_str, "{}\n")
+ for k,v in pairs(value) do -- save its fields
+ local fieldname = string.format("%s[%s]", name, basicSerialize(k))
+ table.insert(t_str, mist.utils.serializeWithCycles(fieldname, v, saved))
+ end
+ end
+ end
+ return table.concat(t_str)
+ else
+ return ""
+ end
+end
+
+--- Serialize a table to a single line string.
+-- serialization of a table all on a single line, no comments, made to replace old get_table_string function
+-- borrowed from slmod
+-- @tparam table tbl table to serialize.
+-- @treturn string string containing serialized table
+function mist.utils.oneLineSerialize(tbl)
+ if type(tbl) == 'table' then --function only works for tables!
+
+ local tbl_str = {}
+
+ tbl_str[#tbl_str + 1] = '{ '
+
+ for ind,val in pairs(tbl) do -- serialize its fields
+ if type(ind) == "number" then
+ tbl_str[#tbl_str + 1] = '['
+ tbl_str[#tbl_str + 1] = tostring(ind)
+ tbl_str[#tbl_str + 1] = '] = '
+ else --must be a string
+ tbl_str[#tbl_str + 1] = '['
+ tbl_str[#tbl_str + 1] = mist.utils.basicSerialize(ind)
+ tbl_str[#tbl_str + 1] = '] = '
+ end
+
+ if ((type(val) == 'number') or (type(val) == 'boolean')) then
+ tbl_str[#tbl_str + 1] = tostring(val)
+ tbl_str[#tbl_str + 1] = ', '
+ elseif type(val) == 'string' then
+ tbl_str[#tbl_str + 1] = mist.utils.basicSerialize(val)
+ tbl_str[#tbl_str + 1] = ', '
+ elseif type(val) == 'nil' then -- won't ever happen, right?
+ tbl_str[#tbl_str + 1] = 'nil, '
+ elseif type(val) == 'table' then
+ tbl_str[#tbl_str + 1] = mist.utils.oneLineSerialize(val)
+ tbl_str[#tbl_str + 1] = ', ' --I think this is right, I just added it
+ else
+ log:war('Unable to serialize value type $1 at index $2', mist.utils.basicSerialize(type(val)), tostring(ind))
+ end
+
+ end
+ tbl_str[#tbl_str + 1] = '}'
+ return table.concat(tbl_str)
+ end
+end
+
+--- Returns table in a easy readable string representation.
+-- this function is not meant for serialization because it uses
+-- newlines for better readability.
+-- @param tbl table to show
+-- @param loc
+-- @param indent
+-- @param tableshow_tbls
+-- @return human readable string representation of given table
+function mist.utils.tableShow(tbl, loc, indent, tableshow_tbls) --based on serialize_slmod, this is a _G serialization
+ tableshow_tbls = tableshow_tbls or {} --create table of tables
+ loc = loc or ""
+ indent = indent or ""
+ if type(tbl) == 'table' then --function only works for tables!
+ tableshow_tbls[tbl] = loc
+
+ local tbl_str = {}
+
+ tbl_str[#tbl_str + 1] = indent .. '{\n'
+
+ for ind,val in pairs(tbl) do -- serialize its fields
+ if type(ind) == "number" then
+ tbl_str[#tbl_str + 1] = indent
+ tbl_str[#tbl_str + 1] = loc .. '['
+ tbl_str[#tbl_str + 1] = tostring(ind)
+ tbl_str[#tbl_str + 1] = '] = '
+ else
+ tbl_str[#tbl_str + 1] = indent
+ tbl_str[#tbl_str + 1] = loc .. '['
+ tbl_str[#tbl_str + 1] = mist.utils.basicSerialize(ind)
+ tbl_str[#tbl_str + 1] = '] = '
+ end
+
+ if ((type(val) == 'number') or (type(val) == 'boolean')) then
+ tbl_str[#tbl_str + 1] = tostring(val)
+ tbl_str[#tbl_str + 1] = ',\n'
+ elseif type(val) == 'string' then
+ tbl_str[#tbl_str + 1] = mist.utils.basicSerialize(val)
+ tbl_str[#tbl_str + 1] = ',\n'
+ elseif type(val) == 'nil' then -- won't ever happen, right?
+ tbl_str[#tbl_str + 1] = 'nil,\n'
+ elseif type(val) == 'table' then
+ if tableshow_tbls[val] then
+ tbl_str[#tbl_str + 1] = tostring(val) .. ' already defined: ' .. tableshow_tbls[val] .. ',\n'
+ else
+ tableshow_tbls[val] = loc .. '[' .. mist.utils.basicSerialize(ind) .. ']'
+ tbl_str[#tbl_str + 1] = tostring(val) .. ' '
+ tbl_str[#tbl_str + 1] = mist.utils.tableShow(val, loc .. '[' .. mist.utils.basicSerialize(ind).. ']', indent .. ' ', tableshow_tbls)
+ tbl_str[#tbl_str + 1] = ',\n'
+ end
+ elseif type(val) == 'function' then
+ if debug and debug.getinfo then
+ local fcnname = tostring(val)
+ local info = debug.getinfo(val, "S")
+ if info.what == "C" then
+ tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', C function') .. ',\n'
+ else
+ if (string.sub(info.source, 1, 2) == [[./]]) then
+ tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', defined in (' .. info.linedefined .. '-' .. info.lastlinedefined .. ')' .. info.source) ..',\n'
+ else
+ tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', defined in (' .. info.linedefined .. '-' .. info.lastlinedefined .. ')') ..',\n'
+ end
+ end
+
+ else
+ tbl_str[#tbl_str + 1] = 'a function,\n'
+ end
+ else
+ tbl_str[#tbl_str + 1] = 'unable to serialize value type ' .. mist.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)
+ end
+ end
+
+ tbl_str[#tbl_str + 1] = indent .. '}'
+ return table.concat(tbl_str)
+ end
+end
+end
+
+--- Debug functions
+-- @section mist.debug
+do -- mist.debug scope
+ mist.debug = {}
+
+ --- Dumps the global table _G.
+ -- This dumps the global table _G to a file in
+ -- the DCS\Logs directory.
+ -- This function requires you to disable script sanitization
+ -- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io
+ -- libraries.
+ -- @param fname
+ function mist.debug.dump_G(fname)
+ if lfs and io then
+ local fdir = lfs.writedir() .. [[Logs\]] .. fname
+ local f = io.open(fdir, 'w')
+ f:write(mist.utils.tableShow(_G))
+ f:close()
+ log:info('Wrote debug data to $1', fdir)
+ --trigger.action.outText(errmsg, 10)
+ else
+ log:alert('insufficient libraries to run mist.debug.dump_G, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua')
+ --trigger.action.outText(errmsg, 10)
+ end
+ end
+
+ --- Write debug data to file.
+ -- This function requires you to disable script sanitization
+ -- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io
+ -- libraries.
+ -- @param fcn
+ -- @param fcnVars
+ -- @param fname
+ function mist.debug.writeData(fcn, fcnVars, fname)
+ if lfs and io then
+ local fdir = lfs.writedir() .. [[Logs\]] .. fname
+ local f = io.open(fdir, 'w')
+ f:write(fcn(unpack(fcnVars, 1, table.maxn(fcnVars))))
+ f:close()
+ log:info('Wrote debug data to $1', fdir)
+ local errmsg = 'mist.debug.writeData wrote data to ' .. fdir
+ trigger.action.outText(errmsg, 10)
+ else
+ local errmsg = 'Error: insufficient libraries to run mist.debug.writeData, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua'
+ log:alert(errmsg)
+ trigger.action.outText(errmsg, 10)
+ end
+ end
+
+ --- Write mist databases to file.
+ -- This function requires you to disable script sanitization
+ -- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io
+ -- libraries.
+ function mist.debug.dumpDBs()
+ for DBname, DB in pairs(mist.DBs) do
+ if type(DB) == 'table' and type(DBname) == 'string' then
+ mist.debug.writeData(mist.utils.serialize, {DBname, DB}, 'mist_DBs_' .. DBname .. '.lua')
+ end
+ end
+ end
+end
+
+--- 3D Vector functions
+-- @section mist.vec
+do -- mist.vec scope
+ mist.vec = {}
+
+ --- Vector addition.
+ -- @tparam Vec3 vec1 first vector
+ -- @tparam Vec3 vec2 second vector
+ -- @treturn Vec3 new vector, sum of vec1 and vec2.
+ function mist.vec.add(vec1, vec2)
+ return {x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z}
+ end
+
+ --- Vector substraction.
+ -- @tparam Vec3 vec1 first vector
+ -- @tparam Vec3 vec2 second vector
+ -- @treturn Vec3 new vector, vec2 substracted from vec1.
+ function mist.vec.sub(vec1, vec2)
+ return {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z}
+ end
+
+ --- Vector scalar multiplication.
+ -- @tparam Vec3 vec vector to multiply
+ -- @tparam number mult scalar multiplicator
+ -- @treturn Vec3 new vector multiplied with the given scalar
+ function mist.vec.scalarMult(vec, mult)
+ return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult}
+ end
+
+ mist.vec.scalar_mult = mist.vec.scalarMult
+
+ --- Vector dot product.
+ -- @tparam Vec3 vec1 first vector
+ -- @tparam Vec3 vec2 second vector
+ -- @treturn number dot product of given vectors
+ function mist.vec.dp (vec1, vec2)
+ return vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z
+ end
+
+ --- Vector cross product.
+ -- @tparam Vec3 vec1 first vector
+ -- @tparam Vec3 vec2 second vector
+ -- @treturn Vec3 new vector, cross product of vec1 and vec2.
+ function mist.vec.cp(vec1, vec2)
+ return { x = vec1.y*vec2.z - vec1.z*vec2.y, y = vec1.z*vec2.x - vec1.x*vec2.z, z = vec1.x*vec2.y - vec1.y*vec2.x}
+ end
+
+ --- Vector magnitude
+ -- @tparam Vec3 vec vector
+ -- @treturn number magnitude of vector vec
+ function mist.vec.mag(vec)
+ return (vec.x^2 + vec.y^2 + vec.z^2)^0.5
+ end
+
+ --- Unit vector
+ -- @tparam Vec3 vec
+ -- @treturn Vec3 unit vector of vec
+ function mist.vec.getUnitVec(vec)
+ local mag = mist.vec.mag(vec)
+ return { x = vec.x/mag, y = vec.y/mag, z = vec.z/mag }
+ end
+
+ --- Rotate vector.
+ -- @tparam Vec2 vec2 to rotoate
+ -- @tparam number theta
+ -- @return Vec2 rotated vector.
+ function mist.vec.rotateVec2(vec2, theta)
+ return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)}
+ end
+end
+
+--- Flag functions.
+-- The mist "Flag functions" are functions that are similar to Slmod functions
+-- that detect a game condition and set a flag when that game condition is met.
+--
+-- They are intended to be used by persons with little or no experience in Lua
+-- programming, but with a good knowledge of the DCS mission editor.
+-- @section mist.flagFunc
+do -- mist.flagFunc scope
+ mist.flagFunc = {}
+
+ --- Sets a flag if map objects are destroyed inside a zone.
+ -- Once this function is run, it will start a continuously evaluated process
+ -- that will set a flag true if map objects (such as bridges, buildings in
+ -- town, etc.) die (or have died) in a mission editor zone (or set of zones).
+ -- This will only happen once; once the flag is set true, the process ends.
+ -- @usage
+ -- -- Example vars table
+ -- vars = {
+ -- zones = { "zone1", "zone2" }, -- can also be a single string
+ -- flag = 3, -- number of the flag
+ -- stopflag = 4, -- optional number of the stop flag
+ -- req_num = 10, -- optional minimum amount of map objects needed to die
+ -- }
+ -- mist.flagFuncs.mapobjs_dead_zones(vars)
+ -- @tparam table vars table containing parameters.
+ function mist.flagFunc.mapobjs_dead_zones(vars)
+ --[[vars needs to be:
+zones = table or string,
+flag = number,
+stopflag = number or nil,
+req_num = number or nil
+
+AND used by function,
+initial_number
+
+]]
+ -- type_tbl
+ local type_tbl = {
+ [{'zones', 'zone'}] = {'table', 'string'},
+ flag = {'number', 'string'},
+ [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},
+ [{'req_num', 'reqnum'}] = {'number', 'nil'},
+ }
+
+ local err, errmsg = mist.utils.typeCheck('mist.flagFunc.mapobjs_dead_zones', type_tbl, vars)
+ assert(err, errmsg)
+ local zones = vars.zones or vars.zone
+ local flag = vars.flag
+ local stopflag = vars.stopflag or vars.stopFlag or -1
+ local req_num = vars.req_num or vars.reqnum or 1
+ local initial_number = vars.initial_number
+
+ if type(zones) == 'string' then
+ zones = {zones}
+ end
+
+ if not initial_number then
+ initial_number = #mist.getDeadMapObjsInZones(zones)
+ end
+
+ if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
+ if (#mist.getDeadMapObjsInZones(zones) - initial_number) >= req_num and trigger.misc.getUserFlag(flag) == 0 then
+ trigger.action.setUserFlag(flag, true)
+ return
+ else
+ mist.scheduleFunction(mist.flagFunc.mapobjs_dead_zones, {{zones = zones, flag = flag, stopflag = stopflag, req_num = req_num, initial_number = initial_number}}, timer.getTime() + 1)
+ end
+ end
+ end
+
+ --- Sets a flag if map objects are destroyed inside a polygon.
+ -- Once this function is run, it will start a continuously evaluated process
+ -- that will set a flag true if map objects (such as bridges, buildings in
+ -- town, etc.) die (or have died) in a polygon.
+ -- This will only happen once; once the flag is set true, the process ends.
+ -- @usage
+ -- -- Example vars table
+ -- vars = {
+ -- zone = {
+ -- [1] = mist.DBs.unitsByName['NE corner'].point,
+ -- [2] = mist.DBs.unitsByName['SE corner'].point,
+ -- [3] = mist.DBs.unitsByName['SW corner'].point,
+ -- [4] = mist.DBs.unitsByName['NW corner'].point
+ -- }
+ -- flag = 3, -- number of the flag
+ -- stopflag = 4, -- optional number of the stop flag
+ -- req_num = 10, -- optional minimum amount of map objects needed to die
+ -- }
+ -- mist.flagFuncs.mapobjs_dead_zones(vars)
+ -- @tparam table vars table containing parameters.
+ function mist.flagFunc.mapobjs_dead_polygon(vars)
+ --[[vars needs to be:
+zone = table,
+flag = number,
+stopflag = number or nil,
+req_num = number or nil
+
+AND used by function,
+initial_number
+
+]]
+ -- type_tbl
+ local type_tbl = {
+ [{'zone', 'polyzone'}] = 'table',
+ flag = {'number', 'string'},
+ [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},
+ [{'req_num', 'reqnum'}] = {'number', 'nil'},
+ }
+
+ local err, errmsg = mist.utils.typeCheck('mist.flagFunc.mapobjs_dead_polygon', type_tbl, vars)
+ assert(err, errmsg)
+ local zone = vars.zone or vars.polyzone
+ local flag = vars.flag
+ local stopflag = vars.stopflag or vars.stopFlag or -1
+ local req_num = vars.req_num or vars.reqnum or 1
+ local initial_number = vars.initial_number
+
+ if not initial_number then
+ initial_number = #mist.getDeadMapObjsInPolygonZone(zone)
+ end
+
+ if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
+ if (#mist.getDeadMapObjsInPolygonZone(zone) - initial_number) >= req_num and trigger.misc.getUserFlag(flag) == 0 then
+ trigger.action.setUserFlag(flag, true)
+ return
+ else
+ mist.scheduleFunction(mist.flagFunc.mapobjs_dead_polygon, {{zone = zone, flag = flag, stopflag = stopflag, req_num = req_num, initial_number = initial_number}}, timer.getTime() + 1)
+ end
+ end
+ end
+
+ --- Sets a flag if unit(s) is/are inside a polygon.
+ -- @tparam table vars @{unitsInPolygonVars}
+ -- @usage -- set flag 11 to true as soon as any blue vehicles
+ -- -- are inside the polygon shape created off of the waypoints
+ -- -- of the group forest1
+ -- mist.flagFunc.units_in_polygon {
+ -- units = {'[blue][vehicle]'},
+ -- zone = mist.getGroupPoints('forest1'),
+ -- flag = 11
+ -- }
+ function mist.flagFunc.units_in_polygon(vars)
+ --[[vars needs to be:
+units = table,
+zone = table,
+flag = number,
+stopflag = number or nil,
+maxalt = number or nil,
+interval = number or nil,
+req_num = number or nil
+toggle = boolean or nil
+unitTableDef = table or nil
+]]
+ -- type_tbl
+ local type_tbl = {
+ [{'units', 'unit'}] = 'table',
+ [{'zone', 'polyzone'}] = 'table',
+ flag = {'number', 'string'},
+ [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},
+ [{'maxalt', 'alt'}] = {'number', 'nil'},
+ interval = {'number', 'nil'},
+ [{'req_num', 'reqnum'}] = {'number', 'nil'},
+ toggle = {'boolean', 'nil'},
+ unitTableDef = {'table', 'nil'},
+ }
+
+ local err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_in_polygon', type_tbl, vars)
+ assert(err, errmsg)
+ local units = vars.units or vars.unit
+ local zone = vars.zone or vars.polyzone
+ local flag = vars.flag
+ local stopflag = vars.stopflag or vars.stopFlag or -1
+ local interval = vars.interval or 1
+ local maxalt = vars.maxalt or vars.alt
+ local req_num = vars.req_num or vars.reqnum or 1
+ local toggle = vars.toggle or nil
+ local unitTableDef = vars.unitTableDef
+
+ if not units.processed then
+ unitTableDef = mist.utils.deepCopy(units)
+ end
+
+ if (units.processed and units.processed < mist.getLastDBUpdateTime()) or not units.processed then -- run unit table short cuts
+ if unitTableDef then
+ units = mist.makeUnitTable(unitTableDef)
+ end
+ end
+
+ if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == 0) then
+ local num_in_zone = 0
+ for i = 1, #units do
+ local unit = Unit.getByName(units[i])
+ if unit then
+ local pos = unit:getPosition().p
+ if mist.pointInPolygon(pos, zone, maxalt) then
+ num_in_zone = num_in_zone + 1
+ if num_in_zone >= req_num and trigger.misc.getUserFlag(flag) == 0 then
+ trigger.action.setUserFlag(flag, true)
+ break
+ end
+ end
+ end
+ end
+ if toggle and (num_in_zone < req_num) and trigger.misc.getUserFlag(flag) > 0 then
+ trigger.action.setUserFlag(flag, false)
+ end
+ -- do another check in case stopflag was set true by this function
+ if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == 0) then
+ mist.scheduleFunction(mist.flagFunc.units_in_polygon, {{units = units, zone = zone, flag = flag, stopflag = stopflag, interval = interval, req_num = req_num, maxalt = maxalt, toggle = toggle, unitTableDef = unitTableDef}}, timer.getTime() + interval)
+ end
+ end
+
+ end
+
+ --- Sets a flag if unit(s) is/are inside a trigger zone.
+ -- @todo document
+ function mist.flagFunc.units_in_zones(vars)
+ --[[vars needs to be:
+ units = table,
+ zones = table,
+ flag = number,
+ stopflag = number or nil,
+ zone_type = string or nil,
+ req_num = number or nil,
+ interval = number or nil
+ toggle = boolean or nil
+ ]]
+ -- type_tbl
+ local type_tbl = {
+ units = 'table',
+ zones = 'table',
+ flag = {'number', 'string'},
+ [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},
+ [{'zone_type', 'zonetype'}] = {'string', 'nil'},
+ [{'req_num', 'reqnum'}] = {'number', 'nil'},
+ interval = {'number', 'nil'},
+ toggle = {'boolean', 'nil'},
+ unitTableDef = {'table', 'nil'},
+ }
+
+ local err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_in_zones', type_tbl, vars)
+ assert(err, errmsg)
+ local units = vars.units
+ local zones = vars.zones
+ local flag = vars.flag
+ local stopflag = vars.stopflag or vars.stopFlag or -1
+ local zone_type = vars.zone_type or vars.zonetype or 'cylinder'
+ local req_num = vars.req_num or vars.reqnum or 1
+ local interval = vars.interval or 1
+ local toggle = vars.toggle or nil
+ local unitTableDef = vars.unitTableDef
+
+ if not units.processed then
+ unitTableDef = mist.utils.deepCopy(units)
+ end
+
+ if (units.processed and units.processed < mist.getLastDBUpdateTime()) or not units.processed then -- run unit table short cuts
+ if unitTableDef then
+ units = mist.makeUnitTable(unitTableDef)
+ end
+ end
+
+ if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
+
+ local in_zone_units = mist.getUnitsInZones(units, zones, zone_type)
+
+ if #in_zone_units >= req_num and trigger.misc.getUserFlag(flag) == 0 then
+ trigger.action.setUserFlag(flag, true)
+ elseif #in_zone_units < req_num and toggle then
+ trigger.action.setUserFlag(flag, false)
+ end
+ -- do another check in case stopflag was set true by this function
+ if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
+ mist.scheduleFunction(mist.flagFunc.units_in_zones, {{units = units, zones = zones, flag = flag, stopflag = stopflag, zone_type = zone_type, req_num = req_num, interval = interval, toggle = toggle, unitTableDef = unitTableDef}}, timer.getTime() + interval)
+ end
+ end
+
+ end
+
+ --- Sets a flag if unit(s) is/are inside a moving zone.
+ -- @todo document
+ function mist.flagFunc.units_in_moving_zones(vars)
+ --[[vars needs to be:
+ units = table,
+ zone_units = table,
+ radius = number,
+ flag = number,
+ stopflag = number or nil,
+ zone_type = string or nil,
+ req_num = number or nil,
+ interval = number or nil
+ toggle = boolean or nil
+ ]]
+ -- type_tbl
+ local type_tbl = {
+ units = 'table',
+ [{'zone_units', 'zoneunits'}] = 'table',
+ radius = 'number',
+ flag = {'number', 'string'},
+ [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},
+ [{'zone_type', 'zonetype'}] = {'string', 'nil'},
+ [{'req_num', 'reqnum'}] = {'number', 'nil'},
+ interval = {'number', 'nil'},
+ toggle = {'boolean', 'nil'},
+ unitTableDef = {'table', 'nil'},
+ zUnitTableDef = {'table', 'nil'},
+ }
+
+ local err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_in_moving_zones', type_tbl, vars)
+ assert(err, errmsg)
+ local units = vars.units
+ local zone_units = vars.zone_units or vars.zoneunits
+ local radius = vars.radius
+ local flag = vars.flag
+ local stopflag = vars.stopflag or vars.stopFlag or -1
+ local zone_type = vars.zone_type or vars.zonetype or 'cylinder'
+ local req_num = vars.req_num or vars.reqnum or 1
+ local interval = vars.interval or 1
+ local toggle = vars.toggle or nil
+ local unitTableDef = vars.unitTableDef
+ local zUnitTableDef = vars.zUnitTableDef
+
+ if not units.processed then
+ unitTableDef = mist.utils.deepCopy(units)
+ end
+
+ if not zone_units.processed then
+ zUnitTableDef = mist.utils.deepCopy(zone_units)
+ end
+
+ if (units.processed and units.processed < mist.getLastDBUpdateTime()) or not units.processed then -- run unit table short cuts
+ if unitTableDef then
+ units = mist.makeUnitTable(unitTableDef)
+ end
+ end
+
+ if (zone_units.processed and zone_units.processed < mist.getLastDBUpdateTime()) or not zone_units.processed then -- run unit table short cuts
+ if zUnitTableDef then
+ zone_units = mist.makeUnitTable(zUnitTableDef)
+ end
+
+ end
+
+ if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
+
+ local in_zone_units = mist.getUnitsInMovingZones(units, zone_units, radius, zone_type)
+
+ if #in_zone_units >= req_num and trigger.misc.getUserFlag(flag) == 0 then
+ trigger.action.setUserFlag(flag, true)
+ elseif #in_zone_units < req_num and toggle then
+ trigger.action.setUserFlag(flag, false)
+ end
+ -- do another check in case stopflag was set true by this function
+ if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
+ mist.scheduleFunction(mist.flagFunc.units_in_moving_zones, {{units = units, zone_units = zone_units, radius = radius, flag = flag, stopflag = stopflag, zone_type = zone_type, req_num = req_num, interval = interval, toggle = toggle, unitTableDef = unitTableDef, zUnitTableDef = zUnitTableDef}}, timer.getTime() + interval)
+ end
+ end
+
+ end
+
+ --- Sets a flag if units have line of sight to each other.
+ -- @todo document
+ function mist.flagFunc.units_LOS(vars)
+ --[[vars needs to be:
+unitset1 = table,
+altoffset1 = number,
+unitset2 = table,
+altoffset2 = number,
+flag = number,
+stopflag = number or nil,
+radius = number or nil,
+interval = number or nil,
+req_num = number or nil
+toggle = boolean or nil
+]]
+ -- type_tbl
+ local type_tbl = {
+ [{'unitset1', 'units1'}] = 'table',
+ [{'altoffset1', 'alt1'}] = 'number',
+ [{'unitset2', 'units2'}] = 'table',
+ [{'altoffset2', 'alt2'}] = 'number',
+ flag = {'number', 'string'},
+ [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},
+ [{'req_num', 'reqnum'}] = {'number', 'nil'},
+ interval = {'number', 'nil'},
+ radius = {'number', 'nil'},
+ toggle = {'boolean', 'nil'},
+ unitTableDef1 = {'table', 'nil'},
+ unitTableDef2 = {'table', 'nil'},
+ }
+
+ local err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_LOS', type_tbl, vars)
+ assert(err, errmsg)
+ local unitset1 = vars.unitset1 or vars.units1
+ local altoffset1 = vars.altoffset1 or vars.alt1
+ local unitset2 = vars.unitset2 or vars.units2
+ local altoffset2 = vars.altoffset2 or vars.alt2
+ local flag = vars.flag
+ local stopflag = vars.stopflag or vars.stopFlag or -1
+ local interval = vars.interval or 1
+ local radius = vars.radius or math.huge
+ local req_num = vars.req_num or vars.reqnum or 1
+ local toggle = vars.toggle or nil
+ local unitTableDef1 = vars.unitTableDef1
+ local unitTableDef2 = vars.unitTableDef2
+
+ if not unitset1.processed then
+ unitTableDef1 = mist.utils.deepCopy(unitset1)
+ end
+
+ if not unitset2.processed then
+ unitTableDef2 = mist.utils.deepCopy(unitset2)
+ end
+
+ if (unitset1.processed and unitset1.processed < mist.getLastDBUpdateTime()) or not unitset1.processed then -- run unit table short cuts
+ if unitTableDef1 then
+ unitset1 = mist.makeUnitTable(unitTableDef1)
+ end
+ end
+
+ if (unitset2.processed and unitset2.processed < mist.getLastDBUpdateTime()) or not unitset2.processed then -- run unit table short cuts
+ if unitTableDef2 then
+ unitset2 = mist.makeUnitTable(unitTableDef2)
+ end
+ end
+
+
+ if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
+
+ local unitLOSdata = mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius)
+
+ if #unitLOSdata >= req_num and trigger.misc.getUserFlag(flag) == 0 then
+ trigger.action.setUserFlag(flag, true)
+ elseif #unitLOSdata < req_num and toggle then
+ trigger.action.setUserFlag(flag, false)
+ end
+ -- do another check in case stopflag was set true by this function
+ if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
+ mist.scheduleFunction(mist.flagFunc.units_LOS, {{unitset1 = unitset1, altoffset1 = altoffset1, unitset2 = unitset2, altoffset2 = altoffset2, flag = flag, stopflag = stopflag, radius = radius, req_num = req_num, interval = interval, toggle = toggle, unitTableDef1 = unitTableDef1, unitTableDef2 = unitTableDef2}}, timer.getTime() + interval)
+ end
+ end
+ end
+
+ --- Sets a flag if group is alive.
+ -- @todo document
+ function mist.flagFunc.group_alive(vars)
+ --[[vars
+groupName
+flag
+toggle
+interval
+stopFlag
+
+]]
+ local type_tbl = {
+ [{'group', 'groupname', 'gp', 'groupName'}] = 'string',
+ flag = {'number', 'string'},
+ [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},
+ interval = {'number', 'nil'},
+ toggle = {'boolean', 'nil'},
+ }
+
+ local err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_alive', type_tbl, vars)
+ assert(err, errmsg)
+
+ local groupName = vars.groupName or vars.group or vars.gp or vars.Groupname
+ local flag = vars.flag
+ local stopflag = vars.stopflag or vars.stopFlag or -1
+ local interval = vars.interval or 1
+ local toggle = vars.toggle or nil
+
+
+ if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
+ if Group.getByName(groupName) and Group.getByName(groupName):isExist() == true and #Group.getByName(groupName):getUnits() > 0 then
+ if trigger.misc.getUserFlag(flag) == 0 then
+ trigger.action.setUserFlag(flag, true)
+ end
+ else
+ if toggle then
+ trigger.action.setUserFlag(flag, false)
+ end
+ end
+ end
+
+ if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
+ mist.scheduleFunction(mist.flagFunc.group_alive, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle}}, timer.getTime() + interval)
+ end
+
+ end
+
+ --- Sets a flag if group is dead.
+ -- @todo document
+ function mist.flagFunc.group_dead(vars)
+ local type_tbl = {
+ [{'group', 'groupname', 'gp', 'groupName'}] = 'string',
+ flag = {'number', 'string'},
+ [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},
+ interval = {'number', 'nil'},
+ toggle = {'boolean', 'nil'},
+ }
+
+ local err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_dead', type_tbl, vars)
+ assert(err, errmsg)
+
+ local groupName = vars.groupName or vars.group or vars.gp or vars.Groupname
+ local flag = vars.flag
+ local stopflag = vars.stopflag or vars.stopFlag or -1
+ local interval = vars.interval or 1
+ local toggle = vars.toggle or nil
+
+
+ if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
+ if (Group.getByName(groupName) and Group.getByName(groupName):isExist() == false) or (Group.getByName(groupName) and #Group.getByName(groupName):getUnits() < 1) or not Group.getByName(groupName) then
+ if trigger.misc.getUserFlag(flag) == 0 then
+ trigger.action.setUserFlag(flag, true)
+ end
+ else
+ if toggle then
+ trigger.action.setUserFlag(flag, false)
+ end
+ end
+ end
+
+ if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
+ mist.scheduleFunction(mist.flagFunc.group_dead, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle}}, timer.getTime() + interval)
+ end
+ end
+
+ --- Sets a flag if less than given percent of group is alive.
+ -- @todo document
+ function mist.flagFunc.group_alive_less_than(vars)
+ local type_tbl = {
+ [{'group', 'groupname', 'gp', 'groupName'}] = 'string',
+ percent = 'number',
+ flag = {'number', 'string'},
+ [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},
+ interval = {'number', 'nil'},
+ toggle = {'boolean', 'nil'},
+ }
+
+ local err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_alive_less_than', type_tbl, vars)
+ assert(err, errmsg)
+
+ local groupName = vars.groupName or vars.group or vars.gp or vars.Groupname
+ local flag = vars.flag
+ local percent = vars.percent
+ local stopflag = vars.stopflag or vars.stopFlag or -1
+ local interval = vars.interval or 1
+ local toggle = vars.toggle or nil
+
+
+ if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
+ if Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then
+ if Group.getByName(groupName):getSize()/Group.getByName(groupName):getInitialSize() < percent/100 then
+ if trigger.misc.getUserFlag(flag) == 0 then
+ trigger.action.setUserFlag(flag, true)
+ end
+ else
+ if toggle then
+ trigger.action.setUserFlag(flag, false)
+ end
+ end
+ else
+ if trigger.misc.getUserFlag(flag) == 0 then
+ trigger.action.setUserFlag(flag, true)
+ end
+ end
+ end
+
+ if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
+ mist.scheduleFunction(mist.flagFunc.group_alive_less_than, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle, percent = percent}}, timer.getTime() + interval)
+ end
+ end
+
+ --- Sets a flag if more than given percent of group is alive.
+ -- @todo document
+ function mist.flagFunc.group_alive_more_than(vars)
+ local type_tbl = {
+ [{'group', 'groupname', 'gp', 'groupName'}] = 'string',
+ percent = 'number',
+ flag = {'number', 'string'},
+ [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},
+ interval = {'number', 'nil'},
+ toggle = {'boolean', 'nil'},
+ }
+
+ local err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_alive_more_than', type_tbl, vars)
+ assert(err, errmsg)
+
+ local groupName = vars.groupName or vars.group or vars.gp or vars.Groupname
+ local flag = vars.flag
+ local percent = vars.percent
+ local stopflag = vars.stopflag or vars.stopFlag or -1
+ local interval = vars.interval or 1
+ local toggle = vars.toggle or nil
+
+
+ if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
+ if Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then
+ if Group.getByName(groupName):getSize()/Group.getByName(groupName):getInitialSize() > percent/100 then
+ if trigger.misc.getUserFlag(flag) == 0 then
+ trigger.action.setUserFlag(flag, true)
+ end
+ else
+ if toggle and trigger.misc.getUserFlag(flag) == 1 then
+ trigger.action.setUserFlag(flag, false)
+ end
+ end
+ else --- just in case
+ if toggle and trigger.misc.getUserFlag(flag) == 1 then
+ trigger.action.setUserFlag(flag, false)
+ end
+ end
+ end
+
+ if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
+ mist.scheduleFunction(mist.flagFunc.group_alive_more_than, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle, percent = percent}}, timer.getTime() + interval)
+ end
+ end
+
+ mist.flagFunc.mapobjsDeadPolygon = mist.flagFunc.mapobjs_dead_polygon
+ mist.flagFunc.mapobjsDeadZones = mist.flagFunc.Mapobjs_dead_zones
+ mist.flagFunc.unitsInZones = mist.flagFunc.units_in_zones
+ mist.flagFunc.unitsInMovingZones = mist.flagFunc.units_in_moving_zones
+ mist.flagFunc.unitsInPolygon = mist.flagFunc.units_in_polygon
+ mist.flagFunc.unitsLOS = mist.flagFunc.units_LOS
+ mist.flagFunc.groupAlive = mist.flagFunc.group_alive
+ mist.flagFunc.groupDead = mist.flagFunc.group_dead
+ mist.flagFunc.groupAliveMoreThan = mist.flagFunc.group_alive_more_than
+ mist.flagFunc.groupAliveLessThan = mist.flagFunc.group_alive_less_than
+
+end
+
+--- Message functions.
+-- Messaging system
+-- @section mist.msg
+do -- mist.msg scope
+ local messageList = {}
+ -- this defines the max refresh rate of the message box it honestly only needs to
+ -- go faster than this for precision timing stuff (which could be its own function)
+ local messageDisplayRate = 0.1
+ local messageID = 0
+ local displayActive = false
+ local displayFuncId = 0
+
+ local caSlots = false
+ local caMSGtoGroup = false
+
+ if env.mission.groundControl then -- just to be sure?
+ for index, value in pairs(env.mission.groundControl) do
+ if type(value) == 'table' then
+ for roleName, roleVal in pairs(value) do
+ for rIndex, rVal in pairs(roleVal) do
+ if rIndex == 'red' or rIndex == 'blue' then
+ if env.mission.groundControl[index][roleName][rIndex] > 0 then
+ caSlots = true
+ break
+ end
+ end
+ end
+ end
+ elseif type(value) == 'boolean' and value == true then
+ caSlots = true
+ break
+ end
+ end
+ end
+
+ local function mistdisplayV5()
+ --[[thoughts to improve upon
+ event handler based activeClients table.
+ display messages only when there is an update
+ possibly co-routine it.
+ ]]
+ end
+
+ local function mistdisplayV4()
+ local activeClients = {}
+
+ for clientId, clientData in pairs(mist.DBs.humansById) do
+ if Unit.getByName(clientData.unitName) and Unit.getByName(clientData.unitName):isExist() == true then
+ activeClients[clientData.groupId] = clientData.groupName
+ end
+ end
+
+ --[[if caSlots == true and caMSGtoGroup == true then
+
+ end]]
+
+
+ if #messageList > 0 then
+ if displayActive == false then
+ displayActive = true
+ end
+ --mist.debug.writeData(mist.utils.serialize,{'msg', messageList}, 'messageList.lua')
+ local msgTableText = {}
+ local msgTableSound = {}
+
+ for messageId, messageData in pairs(messageList) do
+ if messageData.displayedFor > messageData.displayTime then
+ messageData:remove() -- now using the remove/destroy function.
+ else
+ if messageData.displayedFor then
+ messageData.displayedFor = messageData.displayedFor + messageDisplayRate
+ end
+ local nextSound = 1000
+ local soundIndex = 0
+
+ if messageData.multSound and #messageData.multSound > 0 then
+ for index, sData in pairs(messageData.multSound) do
+ if sData.time <= messageData.displayedFor and sData.played == false and sData.time < nextSound then -- find index of the next sound to be played
+ nextSound = sData.time
+ soundIndex = index
+ end
+ end
+ if soundIndex ~= 0 then
+ messageData.multSound[soundIndex].played = true
+ end
+ end
+
+ for recIndex, recData in pairs(messageData.msgFor) do -- iterate recipiants
+ if recData == 'RED' or recData == 'BLUE' or activeClients[recData] then -- rec exists
+ if messageData.text then -- text
+ if not msgTableText[recData] then -- create table entry for text
+ msgTableText[recData] = {}
+ msgTableText[recData].text = {}
+ if recData == 'RED' or recData == 'BLUE' then
+ msgTableText[recData].text[1] = '-------Combined Arms Message-------- \n'
+ end
+ msgTableText[recData].text[#msgTableText[recData].text + 1] = messageData.text
+ msgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor
+ else -- add to table entry and adjust display time if needed
+ if recData == 'RED' or recData == 'BLUE' then
+ msgTableText[recData].text[#msgTableText[recData].text + 1] = '\n ---------------- Combined Arms Message: \n'
+ else
+ msgTableText[recData].text[#msgTableText[recData].text + 1] = '\n ---------------- \n'
+ end
+ msgTableText[recData].text[#msgTableText[recData].text + 1] = messageData.text
+ if msgTableText[recData].displayTime < messageData.displayTime - messageData.displayedFor then
+ msgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor
+ else
+ msgTableText[recData].displayTime = 1
+ end
+ end
+ end
+ if soundIndex ~= 0 then
+ msgTableSound[recData] = messageData.multSound[soundIndex].file
+ end
+ end
+ end
+
+
+ end
+ end
+ ------- new display
+
+ if caSlots == true and caMSGtoGroup == false then
+ if msgTableText.RED then
+ trigger.action.outTextForCoalition(coalition.side.RED, table.concat(msgTableText.RED.text), msgTableText.RED.displayTime, true)
+
+ end
+ if msgTableText.BLUE then
+ trigger.action.outTextForCoalition(coalition.side.BLUE, table.concat(msgTableText.BLUE.text), msgTableText.BLUE.displayTime, true)
+ end
+ end
+
+ for index, msgData in pairs(msgTableText) do
+ if type(index) == 'number' then -- its a groupNumber
+ trigger.action.outTextForGroup(index, table.concat(msgData.text), msgData.displayTime, true)
+ end
+ end
+ --- new audio
+ if msgTableSound.RED then
+ trigger.action.outSoundForCoalition(coalition.side.RED, msgTableSound.RED)
+ end
+ if msgTableSound.BLUE then
+ trigger.action.outSoundForCoalition(coalition.side.BLUE, msgTableSound.BLUE)
+ end
+
+
+ for index, file in pairs(msgTableSound) do
+ if type(index) == 'number' then -- its a groupNumber
+ trigger.action.outSoundForGroup(index, file)
+ end
+ end
+ else
+ mist.removeFunction(displayFuncId)
+ displayActive = false
+ end
+
+ end
+
+ local typeBase = {
+ ['Mi-8MT'] = {'Mi-8MTV2', 'Mi-8MTV', 'Mi-8'},
+ ['MiG-21Bis'] = {'Mig-21'},
+ ['MiG-15bis'] = {'Mig-15'},
+ ['FW-190D9'] = {'FW-190'},
+ ['Bf-109K-4'] = {'Bf-109'},
+ }
+
+ --[[function mist.setCAGroupMSG(val)
+ if type(val) == 'boolean' then
+ caMSGtoGroup = val
+ return true
+ end
+ return false
+end]]
+
+ mist.message = {
+
+ add = function(vars)
+ local function msgSpamFilter(recList, spamBlockOn)
+ for id, name in pairs(recList) do
+ if name == spamBlockOn then
+ -- log:info('already on recList')
+ return recList
+ end
+ end
+ --log:info('add to recList')
+ table.insert(recList, spamBlockOn)
+ return recList
+ end
+
+ --[[
+ local vars = {}
+ vars.text = 'Hello World'
+ vars.displayTime = 20
+ vars.msgFor = {coa = {'red'}, countries = {'Ukraine', 'Georgia'}, unitTypes = {'A-10C'}}
+ mist.message.add(vars)
+
+ Displays the message for all red coalition players. Players belonging to Ukraine and Georgia, and all A-10Cs on the map
+
+ ]]
+
+
+ local new = {}
+ new.text = vars.text -- The actual message
+ new.displayTime = vars.displayTime -- How long will the message appear for
+ new.displayedFor = 0 -- how long the message has been displayed so far
+ new.name = vars.name -- ID to overwrite the older message (if it exists) Basically it replaces a message that is displayed with new text.
+ new.addedAt = timer.getTime()
+ new.update = true
+
+ if vars.multSound and vars.multSound[1] then
+ new.multSound = vars.multSound
+ else
+ new.multSound = {}
+ end
+
+ if vars.sound or vars.fileName then -- converts old sound file system into new multSound format
+ local sound = vars.sound
+ if vars.fileName then
+ sound = vars.fileName
+ end
+ new.multSound[#new.multSound+1] = {time = 0.1, file = sound}
+ end
+
+ if #new.multSound > 0 then
+ for i, data in pairs(new.multSound) do
+ data.played = false
+ end
+ end
+
+ local newMsgFor = {} -- list of all groups message displays for
+ for forIndex, forData in pairs(vars.msgFor) do
+ for list, listData in pairs(forData) do
+ for clientId, clientData in pairs(mist.DBs.humansById) do
+ forIndex = string.lower(forIndex)
+ if type(listData) == 'string' then
+ listData = string.lower(listData)
+ end
+ if (forIndex == 'coa' and (listData == string.lower(clientData.coalition) or listData == 'all')) or (forIndex == 'countries' and string.lower(clientData.country) == listData) or (forIndex == 'units' and string.lower(clientData.unitName) == listData) then --
+ newMsgFor = msgSpamFilter(newMsgFor, clientData.groupId) -- so units dont get the same message twice if complex rules are given
+ --table.insert(newMsgFor, clientId)
+ elseif forIndex == 'unittypes' then
+ for typeId, typeData in pairs(listData) do
+ local found = false
+ for clientDataEntry, clientDataVal in pairs(clientData) do
+ if type(clientDataVal) == 'string' then
+ if mist.matchString(list, clientDataVal) == true or list == 'all' then
+ local sString = typeData
+ for rName, pTbl in pairs(typeBase) do -- just a quick check to see if the user may have meant something and got the specific type of the unit wrong
+ for pIndex, pName in pairs(pTbl) do
+ if mist.stringMatch(sString, pName) then
+ sString = rName
+ end
+ end
+ end
+ if sString == clientData.type then
+ found = true
+ newMsgFor = msgSpamFilter(newMsgFor, clientData.groupId) -- sends info oto other function to see if client is already recieving the current message.
+ --table.insert(newMsgFor, clientId)
+ end
+ end
+ end
+ if found == true then -- shouldn't this be elsewhere too?
+ break
+ end
+ end
+ end
+
+ end
+ end
+ for coaData, coaId in pairs(coalition.side) do
+ if string.lower(forIndex) == 'coa' or string.lower(forIndex) == 'ca' then
+ if listData == string.lower(coaData) or listData == 'all' then
+ newMsgFor = msgSpamFilter(newMsgFor, coaData)
+ end
+ end
+ end
+ end
+ end
+
+ if #newMsgFor > 0 then
+ new.msgFor = newMsgFor -- I swear its not confusing
+
+ else
+ return false
+ end
+
+
+ if vars.name and type(vars.name) == 'string' then
+ for i = 1, #messageList do
+ if messageList[i].name then
+ if messageList[i].name == vars.name then
+ --log:info('updateMessage')
+ messageList[i].displayedFor = 0
+ messageList[i].addedAt = timer.getTime()
+ messageList[i].sound = new.sound
+ messageList[i].text = new.text
+ messageList[i].msgFor = new.msgFor
+ messageList[i].multSound = new.multSound
+ messageList[i].update = true
+ return messageList[i].messageID
+ end
+ end
+ end
+ end
+
+ messageID = messageID + 1
+ new.messageID = messageID
+
+ --mist.debug.writeData(mist.utils.serialize,{'msg', new}, 'newMsg.lua')
+
+
+ messageList[#messageList + 1] = new
+
+ local mt = { __index = mist.message}
+ setmetatable(new, mt)
+
+ if displayActive == false then
+ displayActive = true
+ displayFuncId = mist.scheduleFunction(mistdisplayV4, {}, timer.getTime() + messageDisplayRate, messageDisplayRate)
+ end
+
+ return messageID
+
+ end,
+
+ remove = function(self) -- Now a self variable; the former functionality taken up by mist.message.removeById.
+ for i, msgData in pairs(messageList) do
+ if messageList[i] == self then
+ table.remove(messageList, i)
+ return true --removal successful
+ end
+ end
+ return false -- removal not successful this script fails at life!
+ end,
+
+ removeById = function(id) -- This function is NOT passed a self variable; it is the remove by id function.
+ for i, msgData in pairs(messageList) do
+ if messageList[i].messageID == id then
+ table.remove(messageList, i)
+ return true --removal successful
+ end
+ end
+ return false -- removal not successful this script fails at life!
+ end,
+ }
+
+ --[[ vars for mist.msgMGRS
+vars.units - table of unit names (NOT unitNameTable- maybe this should change).
+vars.acc - integer between 0 and 5, inclusive
+vars.text - text in the message
+vars.displayTime - self explanatory
+vars.msgFor - scope
+]]
+ function mist.msgMGRS(vars)
+ local units = vars.units
+ local acc = vars.acc
+ local text = vars.text
+ local displayTime = vars.displayTime
+ local msgFor = vars.msgFor
+
+ local s = mist.getMGRSString{units = units, acc = acc}
+ local newText
+ if text then
+ if string.find(text, '%%s') then -- look for %s
+ newText = string.format(text, s) -- insert the coordinates into the message
+ else
+ -- just append to the end.
+ newText = text .. s
+ end
+ else
+ newText = s
+ end
+ mist.message.add{
+ text = newText,
+ displayTime = displayTime,
+ msgFor = msgFor
+ }
+ end
+
+ --[[ vars for mist.msgLL
+vars.units - table of unit names (NOT unitNameTable- maybe this should change) (Yes).
+vars.acc - integer, number of numbers after decimal place
+vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes.
+vars.text - text in the message
+vars.displayTime - self explanatory
+vars.msgFor - scope
+]]
+ function mist.msgLL(vars)
+ local units = vars.units -- technically, I don't really need to do this, but it helps readability.
+ local acc = vars.acc
+ local DMS = vars.DMS
+ local text = vars.text
+ local displayTime = vars.displayTime
+ local msgFor = vars.msgFor
+
+ local s = mist.getLLString{units = units, acc = acc, DMS = DMS}
+ local newText
+ if text then
+ if string.find(text, '%%s') then -- look for %s
+ newText = string.format(text, s) -- insert the coordinates into the message
+ else
+ -- just append to the end.
+ newText = text .. s
+ end
+ else
+ newText = s
+ end
+
+ mist.message.add{
+ text = newText,
+ displayTime = displayTime,
+ msgFor = msgFor
+ }
+
+ end
+
+ --[[
+vars.units- table of unit names (NOT unitNameTable- maybe this should change).
+vars.ref - vec3 ref point, maybe overload for vec2 as well?
+vars.alt - boolean, if used, includes altitude in string
+vars.metric - boolean, gives distance in km instead of NM.
+vars.text - text of the message
+vars.displayTime
+vars.msgFor - scope
+]]
+ function mist.msgBR(vars)
+ local units = vars.units -- technically, I don't really need to do this, but it helps readability.
+ local ref = vars.ref -- vec2/vec3 will be handled in mist.getBRString
+ local alt = vars.alt
+ local metric = vars.metric
+ local text = vars.text
+ local displayTime = vars.displayTime
+ local msgFor = vars.msgFor
+
+ local s = mist.getBRString{units = units, ref = ref, alt = alt, metric = metric}
+ local newText
+ if text then
+ if string.find(text, '%%s') then -- look for %s
+ newText = string.format(text, s) -- insert the coordinates into the message
+ else
+ -- just append to the end.
+ newText = text .. s
+ end
+ else
+ newText = s
+ end
+
+ mist.message.add{
+ text = newText,
+ displayTime = displayTime,
+ msgFor = msgFor
+ }
+
+ end
+
+ -- basically, just sub-types of mist.msgBR... saves folks the work of getting the ref point.
+ --[[
+vars.units- table of unit names (NOT unitNameTable- maybe this should change).
+vars.ref - string red, blue
+vars.alt - boolean, if used, includes altitude in string
+vars.metric - boolean, gives distance in km instead of NM.
+vars.text - text of the message
+vars.displayTime
+vars.msgFor - scope
+]]
+ function mist.msgBullseye(vars)
+ if string.lower(vars.ref) == 'red' then
+ vars.ref = mist.DBs.missionData.bullseye.red
+ mist.msgBR(vars)
+ elseif string.lower(vars.ref) == 'blue' then
+ vars.ref = mist.DBs.missionData.bullseye.blue
+ mist.msgBR(vars)
+ end
+ end
+
+ --[[
+vars.units- table of unit names (NOT unitNameTable- maybe this should change).
+vars.ref - unit name of reference point
+vars.alt - boolean, if used, includes altitude in string
+vars.metric - boolean, gives distance in km instead of NM.
+vars.text - text of the message
+vars.displayTime
+vars.msgFor - scope
+]]
+ function mist.msgBRA(vars)
+ if Unit.getByName(vars.ref) and Unit.getByName(vars.ref):isExist() == true then
+ vars.ref = Unit.getByName(vars.ref):getPosition().p
+ if not vars.alt then
+ vars.alt = true
+ end
+ mist.msgBR(vars)
+ end
+ end
+
+ --[[ vars for mist.msgLeadingMGRS:
+vars.units - table of unit names
+vars.heading - direction
+vars.radius - number
+vars.headingDegrees - boolean, switches heading to degrees (optional)
+vars.acc - number, 0 to 5.
+vars.text - text of the message
+vars.displayTime
+vars.msgFor - scope
+]]
+ function mist.msgLeadingMGRS(vars)
+ local units = vars.units -- technically, I don't really need to do this, but it helps readability.
+ local heading = vars.heading
+ local radius = vars.radius
+ local headingDegrees = vars.headingDegrees
+ local acc = vars.acc
+ local text = vars.text
+ local displayTime = vars.displayTime
+ local msgFor = vars.msgFor
+
+ local s = mist.getLeadingMGRSString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc}
+ local newText
+ if text then
+ if string.find(text, '%%s') then -- look for %s
+ newText = string.format(text, s) -- insert the coordinates into the message
+ else
+ -- just append to the end.
+ newText = text .. s
+ end
+ else
+ newText = s
+ end
+
+ mist.message.add{
+ text = newText,
+ displayTime = displayTime,
+ msgFor = msgFor
+ }
+
+
+ end
+
+ --[[ vars for mist.msgLeadingLL:
+vars.units - table of unit names
+vars.heading - direction, number
+vars.radius - number
+vars.headingDegrees - boolean, switches heading to degrees (optional)
+vars.acc - number of digits after decimal point (can be negative)
+vars.DMS - boolean, true if you want DMS. (optional)
+vars.text - text of the message
+vars.displayTime
+vars.msgFor - scope
+]]
+ function mist.msgLeadingLL(vars)
+ local units = vars.units -- technically, I don't really need to do this, but it helps readability.
+ local heading = vars.heading
+ local radius = vars.radius
+ local headingDegrees = vars.headingDegrees
+ local acc = vars.acc
+ local DMS = vars.DMS
+ local text = vars.text
+ local displayTime = vars.displayTime
+ local msgFor = vars.msgFor
+
+ local s = mist.getLeadingLLString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS}
+ local newText
+
+ if text then
+ if string.find(text, '%%s') then -- look for %s
+ newText = string.format(text, s) -- insert the coordinates into the message
+ else
+ -- just append to the end.
+ newText = text .. s
+ end
+ else
+ newText = s
+ end
+
+ mist.message.add{
+ text = newText,
+ displayTime = displayTime,
+ msgFor = msgFor
+ }
+
+ end
+
+ --[[
+vars.units - table of unit names
+vars.heading - direction, number
+vars.radius - number
+vars.headingDegrees - boolean, switches heading to degrees (optional)
+vars.metric - boolean, if true, use km instead of NM. (optional)
+vars.alt - boolean, if true, include altitude. (optional)
+vars.ref - vec3/vec2 reference point.
+vars.text - text of the message
+vars.displayTime
+vars.msgFor - scope
+]]
+ function mist.msgLeadingBR(vars)
+ local units = vars.units -- technically, I don't really need to do this, but it helps readability.
+ local heading = vars.heading
+ local radius = vars.radius
+ local headingDegrees = vars.headingDegrees
+ local metric = vars.metric
+ local alt = vars.alt
+ local ref = vars.ref -- vec2/vec3 will be handled in mist.getBRString
+ local text = vars.text
+ local displayTime = vars.displayTime
+ local msgFor = vars.msgFor
+
+ local s = mist.getLeadingBRString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref}
+ local newText
+
+ if text then
+ if string.find(text, '%%s') then -- look for %s
+ newText = string.format(text, s) -- insert the coordinates into the message
+ else
+ -- just append to the end.
+ newText = text .. s
+ end
+ else
+ newText = s
+ end
+
+ mist.message.add{
+ text = newText,
+ displayTime = displayTime,
+ msgFor = msgFor
+ }
+ end
+end
+
+--- Demo functions.
+-- @section mist.demos
+do -- mist.demos scope
+ mist.demos = {}
+
+ function mist.demos.printFlightData(unit)
+ if unit:isExist() then
+ local function printData(unit, prevVel, prevE, prevTime)
+ local angles = mist.getAttitude(unit)
+ if angles then
+ local Heading = angles.Heading
+ local Pitch = angles.Pitch
+ local Roll = angles.Roll
+ local Yaw = angles.Yaw
+ local AoA = angles.AoA
+ local ClimbAngle = angles.ClimbAngle
+
+ if not Heading then
+ Heading = 'NA'
+ else
+ Heading = string.format('%12.2f', mist.utils.toDegree(Heading))
+ end
+
+ if not Pitch then
+ Pitch = 'NA'
+ else
+ Pitch = string.format('%12.2f', mist.utils.toDegree(Pitch))
+ end
+
+ if not Roll then
+ Roll = 'NA'
+ else
+ Roll = string.format('%12.2f', mist.utils.toDegree(Roll))
+ end
+
+ local AoAplusYaw = 'NA'
+ if AoA and Yaw then
+ AoAplusYaw = string.format('%12.2f', mist.utils.toDegree((AoA^2 + Yaw^2)^0.5))
+ end
+
+ if not Yaw then
+ Yaw = 'NA'
+ else
+ Yaw = string.format('%12.2f', mist.utils.toDegree(Yaw))
+ end
+
+ if not AoA then
+ AoA = 'NA'
+ else
+ AoA = string.format('%12.2f', mist.utils.toDegree(AoA))
+ end
+
+ if not ClimbAngle then
+ ClimbAngle = 'NA'
+ else
+ ClimbAngle = string.format('%12.2f', mist.utils.toDegree(ClimbAngle))
+ end
+ local unitPos = unit:getPosition()
+ local unitVel = unit:getVelocity()
+ local curTime = timer.getTime()
+ local absVel = string.format('%12.2f', mist.vec.mag(unitVel))
+
+
+ local unitAcc = 'NA'
+ local Gs = 'NA'
+ local axialGs = 'NA'
+ local transGs = 'NA'
+ if prevVel and prevTime then
+ local xAcc = (unitVel.x - prevVel.x)/(curTime - prevTime)
+ local yAcc = (unitVel.y - prevVel.y)/(curTime - prevTime)
+ local zAcc = (unitVel.z - prevVel.z)/(curTime - prevTime)
+
+ unitAcc = string.format('%12.2f', mist.vec.mag({x = xAcc, y = yAcc, z = zAcc}))
+ Gs = string.format('%12.2f', mist.vec.mag({x = xAcc, y = yAcc + 9.81, z = zAcc})/9.81)
+ axialGs = string.format('%12.2f', mist.vec.dp({x = xAcc, y = yAcc + 9.81, z = zAcc}, unitPos.x)/9.81)
+ transGs = string.format('%12.2f', mist.vec.mag(mist.vec.cp({x = xAcc, y = yAcc + 9.81, z = zAcc}, unitPos.x))/9.81)
+ end
+
+ local E = 0.5*mist.vec.mag(unitVel)^2 + 9.81*unitPos.p.y
+
+ local energy = string.format('%12.2e', E)
+
+ local dEdt = 'NA'
+ if prevE and prevTime then
+ dEdt = string.format('%12.2e', (E - prevE)/(curTime - prevTime))
+ end
+
+ trigger.action.outText(string.format('%-25s', 'Heading: ') .. Heading .. ' degrees\n' .. string.format('%-25s', 'Roll: ') .. Roll .. ' degrees\n' .. string.format('%-25s', 'Pitch: ') .. Pitch
+ .. ' degrees\n' .. string.format('%-25s', 'Yaw: ') .. Yaw .. ' degrees\n' .. string.format('%-25s', 'AoA: ') .. AoA .. ' degrees\n' .. string.format('%-25s', 'AoA plus Yaw: ') .. AoAplusYaw .. ' degrees\n' .. string.format('%-25s', 'Climb Angle: ') ..
+ ClimbAngle .. ' degrees\n' .. string.format('%-25s', 'Absolute Velocity: ') .. absVel .. ' m/s\n' .. string.format('%-25s', 'Absolute Acceleration: ') .. unitAcc ..' m/s^2\n'
+ .. string.format('%-25s', 'Axial G loading: ') .. axialGs .. ' g\n' .. string.format('%-25s', 'Transverse G loading: ') .. transGs .. ' g\n' .. string.format('%-25s', 'Absolute G loading: ') .. Gs .. ' g\n' .. string.format('%-25s', 'Energy: ') .. energy .. ' J/kg\n' .. string.format('%-25s', 'dE/dt: ') .. dEdt ..' J/(kg*s)', 1)
+ return unitVel, E, curTime
+ end
+ end
+
+ local function frameFinder(unit, prevVel, prevE, prevTime)
+ if unit:isExist() then
+ local currVel = unit:getVelocity()
+ if prevVel and (prevVel.x ~= currVel.x or prevVel.y ~= currVel.y or prevVel.z ~= currVel.z) or (prevTime and (timer.getTime() - prevTime) > 0.25) then
+ prevVel, prevE, prevTime = printData(unit, prevVel, prevE, prevTime)
+ end
+ mist.scheduleFunction(frameFinder, {unit, prevVel, prevE, prevTime}, timer.getTime() + 0.005) -- it can't go this fast, limited to the 100 times a sec check right now.
+ end
+ end
+
+
+ local curVel = unit:getVelocity()
+ local curTime = timer.getTime()
+ local curE = 0.5*mist.vec.mag(curVel)^2 + 9.81*unit:getPosition().p.y
+ frameFinder(unit, curVel, curE, curTime)
+
+ end
+
+ end
+
+end
+
+--- Time conversion functions.
+-- @section mist.time
+do -- mist.time scope
+ mist.time = {}
+ -- returns a string for specified military time
+ -- theTime is optional
+ -- if present current time in mil time is returned
+ -- if number or table the time is converted into mil tim
+ function mist.time.convertToSec(timeTable)
+
+ timeInSec = 0
+ if timeTable and type(timeTable) == 'number' then
+ timeInSec = timeTable
+ elseif timeTable and type(timeTable) == 'table' and (timeTable.d or timeTable.h or timeTable.m or timeTable.s) then
+ if timeTable.d and type(timeTable.d) == 'number' then
+ timeInSec = timeInSec + (timeTable.d*86400)
+ end
+ if timeTable.h and type(timeTable.h) == 'number' then
+ timeInSec = timeInSec + (timeTable.h*3600)
+ end
+ if timeTable.m and type(timeTable.m) == 'number' then
+ timeInSec = timeInSec + (timeTable.m*60)
+ end
+ if timeTable.s and type(timeTable.s) == 'number' then
+ timeInSec = timeInSec + timeTable.s
+ end
+
+ end
+ return timeInSec
+ end
+
+ function mist.time.getDHMS(timeInSec)
+ if timeInSec and type(timeInSec) == 'number' then
+ local tbl = {d = 0, h = 0, m = 0, s = 0}
+ if timeInSec > 86400 then
+ while timeInSec > 86400 do
+ tbl.d = tbl.d + 1
+ timeInSec = timeInSec - 86400
+ end
+ end
+ if timeInSec > 3600 then
+ while timeInSec > 3600 do
+ tbl.h = tbl.h + 1
+ timeInSec = timeInSec - 3600
+ end
+ end
+ if timeInSec > 60 then
+ while timeInSec > 60 do
+ tbl.m = tbl.m + 1
+ timeInSec = timeInSec - 60
+ end
+ end
+ tbl.s = timeInSec
+ return tbl
+ else
+ log:error("Didn't recieve number")
+ return
+ end
+ end
+
+ function mist.getMilString(theTime)
+ local timeInSec = 0
+ if theTime then
+ timeInSec = mist.time.convertToSec(theTime)
+ else
+ timeInSec = mist.utils.round(timer.getAbsTime(), 0)
+ end
+
+ local DHMS = mist.time.getDHMS(timeInSec)
+
+ return tostring(string.format('%02d', DHMS.h) .. string.format('%02d',DHMS.m))
+ end
+
+ function mist.getClockString(theTime, hour)
+ local timeInSec = 0
+ if theTime then
+ timeInSec = mist.time.convertToSec(theTime)
+ else
+ timeInSec = mist.utils.round(timer.getAbsTime(), 0)
+ end
+ local DHMS = mist.time.getDHMS(timeInSec)
+ if hour then
+ if DHMS.h > 12 then
+ DHMS.h = DHMS.h - 12
+ return tostring(string.format('%02d', DHMS.h) .. ':' .. string.format('%02d',DHMS.m) .. ':' .. string.format('%02d',DHMS.s) .. ' PM')
+ else
+ return tostring(string.format('%02d', DHMS.h) .. ':' .. string.format('%02d',DHMS.m) .. ':' .. string.format('%02d',DHMS.s) .. ' AM')
+ end
+ else
+ return tostring(string.format('%02d', DHMS.h) .. ':' .. string.format('%02d',DHMS.m) .. ':' .. string.format('%02d',DHMS.s))
+ end
+ end
+
+ -- returns the date in string format
+ -- both variables optional
+ -- first val returns with the month as a string
+ -- 2nd val defins if it should be written the American way or the wrong way.
+ function mist.time.getDate(convert)
+ local cal = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} --
+ local date = {}
+
+ if not env.mission.date then -- Not likely to happen. Resaving mission auto updates this to remove it.
+ date.d = 0
+ date.m = 6
+ date.y = 2011
+ else
+ date.d = env.mission.date.Day
+ date.m = env.mission.date.Month
+ date.y = env.mission.date.Year
+ end
+ local start = 86400
+ local timeInSec = mist.utils.round(timer.getAbsTime())
+ if convert and type(convert) == 'number' then
+ timeInSec = convert
+ end
+ if timeInSec > 86400 then
+ while start < timeInSec do
+ if date.d >= cal[date.m] then
+ if date.m == 2 and date.d == 28 then -- HOLY COW we can edit years now. Gotta re-add this!
+ if date.y % 4 == 0 and date.y % 100 == 0 and date.y % 400 ~= 0 or date.y % 4 > 0 then
+ date.m = date.m + 1
+ date.d = 0
+ end
+ --date.d = 29
+ else
+ date.m = date.m + 1
+ date.d = 0
+ end
+ end
+ if date.m == 13 then
+ date.m = 1
+ date.y = date.y + 1
+ end
+ date.d = date.d + 1
+ start = start + 86400
+
+ end
+ end
+ return date
+ end
+
+ function mist.time.relativeToStart(time)
+ if type(time) == 'number' then
+ return time - timer.getTime0()
+ end
+ end
+
+ function mist.getDateString(rtnType, murica, oTime) -- returns date based on time
+ local word = {'January', 'Feburary', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' } -- 'etc
+ local curTime = 0
+ if oTime then
+ curTime = oTime
+ else
+ curTime = mist.utils.round(timer.getAbsTime())
+ end
+ local tbl = mist.time.getDate(curTime)
+
+ if rtnType then
+ if murica then
+ return tostring(word[tbl.m] .. ' ' .. tbl.d .. ' ' .. tbl.y)
+ else
+ return tostring(tbl.d .. ' ' .. word[tbl.m] .. ' ' .. tbl.y)
+ end
+ else
+ if murica then
+ return tostring(tbl.m .. '.' .. tbl.d .. '.' .. tbl.y)
+ else
+ return tostring(tbl.d .. '.' .. tbl.m .. '.' .. tbl.y)
+ end
+ end
+ end
+ --WIP
+ function mist.time.milToGame(milString, rtnType) --converts a military time. By default returns the abosolute time that event would occur. With optional value it returns how many seconds from time of call till that time.
+ local curTime = mist.utils.round(timer.getAbsTime())
+ local milTimeInSec = 0
+
+ if milString and type(milString) == 'string' and string.len(milString) >= 4 then
+ local hr = tonumber(string.sub(milString, 1, 2))
+ local mi = tonumber(string.sub(milString, 3))
+ milTimeInSec = milTimeInSec + (mi*60) + (hr*3600)
+ elseif milString and type(milString) == 'table' and (milString.d or milString.h or milString.m or milString.s) then
+ milTimeInSec = mist.time.convertToSec(milString)
+ end
+
+ local startTime = timer.getTime0()
+ local daysOffset = 0
+ if startTime > 86400 then
+ daysOffset = mist.utils.round(startTime/86400)
+ if daysOffset > 0 then
+ milTimeInSec = milTimeInSec *daysOffset
+ end
+ end
+
+ if curTime > milTimeInSec then
+ milTimeInSec = milTimeInSec + 86400
+ end
+ if rtnType then
+ milTimeInSec = milTimeInSec - startTime
+ end
+ return milTimeInSec
+ end
+
+
+end
+
+--- Group task functions.
+-- @section tasks
+do -- group tasks scope
+ mist.ground = {}
+ mist.fixedWing = {}
+ mist.heli = {}
+ mist.air = {}
+ mist.air.fixedWing = {}
+ mist.air.heli = {}
+
+ --- Tasks group to follow a route.
+ -- This sets the mission task for the given group.
+ -- Any wrapped actions inside the path (like enroute
+ -- tasks) will be executed.
+ -- @tparam Group group group to task.
+ -- @tparam table path containing
+ -- points defining a route.
+ function mist.goRoute(group, path)
+ local misTask = {
+ id = 'Mission',
+ params = {
+ route = {
+ points = mist.utils.deepCopy(path),
+ },
+ },
+ }
+ if type(group) == 'string' then
+ group = Group.getByName(group)
+ end
+ if group then
+ local groupCon = group:getController()
+ if groupCon then
+ groupCon:setTask(misTask)
+ return true
+ end
+ end
+ return false
+ end
+
+ -- same as getGroupPoints but returns speed and formation type along with vec2 of point}
+ function mist.getGroupRoute(groupIdent, task)
+ -- refactor to search by groupId and allow groupId and groupName as inputs
+ local gpId = groupIdent
+ if mist.DBs.MEgroupsByName[groupIdent] then
+ gpId = mist.DBs.MEgroupsByName[groupIdent].groupId
+ else
+ log:error('$1 not found in mist.DBs.MEgroupsByName', groupIdent)
+ end
+
+ for coa_name, coa_data in pairs(env.mission.coalition) do
+ if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then
+ if coa_data.country then --there is a country table
+ for cntry_id, cntry_data in pairs(coa_data.country) do
+ for obj_type_name, obj_type_data in pairs(cntry_data) do
+ if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points
+ if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group!
+ for group_num, group_data in pairs(obj_type_data.group) do
+ if group_data and group_data.groupId == gpId then -- this is the group we are looking for
+ if group_data.route and group_data.route.points and #group_data.route.points > 0 then
+ local points = {}
+
+ for point_num, point in pairs(group_data.route.points) do
+ local routeData = {}
+ if not point.point then
+ routeData.x = point.x
+ routeData.y = point.y
+ else
+ routeData.point = point.point --it's possible that the ME could move to the point = Vec2 notation.
+ end
+ routeData.form = point.action
+ routeData.speed = point.speed
+ routeData.alt = point.alt
+ routeData.alt_type = point.alt_type
+ routeData.airdromeId = point.airdromeId
+ routeData.helipadId = point.helipadId
+ routeData.type = point.type
+ routeData.action = point.action
+ if task then
+ routeData.task = point.task
+ end
+ points[point_num] = routeData
+ end
+
+ return points
+ end
+ log:error('Group route not defined in mission editor for groupId: $1', gpId)
+ return
+ end --if group_data and group_data.name and group_data.name == 'groupname'
+ end --for group_num, group_data in pairs(obj_type_data.group) do
+ end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then
+ end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then
+ end --for obj_type_name, obj_type_data in pairs(cntry_data) do
+ end --for cntry_id, cntry_data in pairs(coa_data.country) do
+ end --if coa_data.country then --there is a country table
+ end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then
+ end --for coa_name, coa_data in pairs(mission.coalition) do
+ end
+
+ -- function mist.ground.buildPath() end -- ????
+
+ function mist.ground.patrolRoute(vars)
+ log:info('patrol')
+ local tempRoute = {}
+ local useRoute = {}
+ local gpData = vars.gpData
+ if type(gpData) == 'string' then
+ gpData = Group.getByName(gpData)
+ end
+
+ local useGroupRoute
+ if not vars.useGroupRoute then
+ useGroupRoute = vars.gpData
+ else
+ useGroupRoute = vars.useGroupRoute
+ end
+ local routeProvided = false
+ if not vars.route then
+ if useGroupRoute then
+ tempRoute = mist.getGroupRoute(useGroupRoute)
+ end
+ else
+ useRoute = vars.route
+ local posStart = mist.getLeadPos(gpData)
+ useRoute[1] = mist.ground.buildWP(posStart, useRoute[1].action, useRoute[1].speed)
+ routeProvided = true
+ end
+
+
+ local overRideSpeed = vars.speed or 'default'
+ local pType = vars.pType
+ local offRoadForm = vars.offRoadForm or 'default'
+ local onRoadForm = vars.onRoadForm or 'default'
+
+ if routeProvided == false and #tempRoute > 0 then
+ local posStart = mist.getLeadPos(gpData)
+
+
+ useRoute[#useRoute + 1] = mist.ground.buildWP(posStart, offRoadForm, overRideSpeed)
+ for i = 1, #tempRoute do
+ local tempForm = tempRoute[i].action
+ local tempSpeed = tempRoute[i].speed
+
+ if offRoadForm == 'default' then
+ tempForm = tempRoute[i].action
+ end
+ if onRoadForm == 'default' then
+ onRoadForm = 'On Road'
+ end
+ if (string.lower(tempRoute[i].action) == 'on road' or string.lower(tempRoute[i].action) == 'onroad' or string.lower(tempRoute[i].action) == 'on_road') then
+ tempForm = onRoadForm
+ else
+ tempForm = offRoadForm
+ end
+
+ if type(overRideSpeed) == 'number' then
+ tempSpeed = overRideSpeed
+ end
+
+
+ useRoute[#useRoute + 1] = mist.ground.buildWP(tempRoute[i], tempForm, tempSpeed)
+ end
+
+ if pType and string.lower(pType) == 'doubleback' then
+ local curRoute = mist.utils.deepCopy(useRoute)
+ for i = #curRoute, 2, -1 do
+ useRoute[#useRoute + 1] = mist.ground.buildWP(curRoute[i], curRoute[i].action, curRoute[i].speed)
+ end
+ end
+
+ useRoute[1].action = useRoute[#useRoute].action -- make it so the first WP matches the last WP
+ end
+
+ local cTask3 = {}
+ local newPatrol = {}
+ newPatrol.route = useRoute
+ newPatrol.gpData = gpData:getName()
+ cTask3[#cTask3 + 1] = 'mist.ground.patrolRoute('
+ cTask3[#cTask3 + 1] = mist.utils.oneLineSerialize(newPatrol)
+ cTask3[#cTask3 + 1] = ')'
+ cTask3 = table.concat(cTask3)
+ local tempTask = {
+ id = 'WrappedAction',
+ params = {
+ action = {
+ id = 'Script',
+ params = {
+ command = cTask3,
+
+ },
+ },
+ },
+ }
+
+ useRoute[#useRoute].task = tempTask
+ log:info(useRoute)
+ mist.goRoute(gpData, useRoute)
+
+ return
+ end
+
+ function mist.ground.patrol(gpData, pType, form, speed)
+ local vars = {}
+
+ if type(gpData) == 'table' and gpData:getName() then
+ gpData = gpData:getName()
+ end
+
+ vars.useGroupRoute = gpData
+ vars.gpData = gpData
+ vars.pType = pType
+ vars.offRoadForm = form
+ vars.speed = speed
+
+ mist.ground.patrolRoute(vars)
+
+ return
+ end
+
+ -- No longer accepts path
+ function mist.ground.buildWP(point, overRideForm, overRideSpeed)
+
+ local wp = {}
+ wp.x = point.x
+
+ if point.z then
+ wp.y = point.z
+ else
+ wp.y = point.y
+ end
+ local form, speed
+
+ if point.speed and not overRideSpeed then
+ wp.speed = point.speed
+ elseif type(overRideSpeed) == 'number' then
+ wp.speed = overRideSpeed
+ else
+ wp.speed = mist.utils.kmphToMps(20)
+ end
+
+ if point.form and not overRideForm then
+ form = point.form
+ else
+ form = overRideForm
+ end
+
+ if not form then
+ wp.action = 'Cone'
+ else
+ form = string.lower(form)
+ if form == 'off_road' or form == 'off road' then
+ wp.action = 'Off Road'
+ elseif form == 'on_road' or form == 'on road' then
+ wp.action = 'On Road'
+ elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then
+ wp.action = 'Rank'
+ elseif form == 'cone' then
+ wp.action = 'Cone'
+ elseif form == 'diamond' then
+ wp.action = 'Diamond'
+ elseif form == 'vee' then
+ wp.action = 'Vee'
+ elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then
+ wp.action = 'EchelonL'
+ elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then
+ wp.action = 'EchelonR'
+ else
+ wp.action = 'Cone' -- if nothing matched
+ end
+ end
+
+ wp.type = 'Turning Point'
+
+ return wp
+
+ end
+
+ function mist.fixedWing.buildWP(point, WPtype, speed, alt, altType)
+
+ local wp = {}
+ wp.x = point.x
+
+ if point.z then
+ wp.y = point.z
+ else
+ wp.y = point.y
+ end
+
+ if alt and type(alt) == 'number' then
+ wp.alt = alt
+ else
+ wp.alt = 2000
+ end
+
+ if altType then
+ altType = string.lower(altType)
+ if altType == 'radio' or altType == 'agl' then
+ wp.alt_type = 'RADIO'
+ elseif altType == 'baro' or altType == 'asl' then
+ wp.alt_type = 'BARO'
+ end
+ else
+ wp.alt_type = 'RADIO'
+ end
+
+ if point.speed then
+ speed = point.speed
+ end
+
+ if point.type then
+ WPtype = point.type
+ end
+
+ if not speed then
+ wp.speed = mist.utils.kmphToMps(500)
+ else
+ wp.speed = speed
+ end
+
+ if not WPtype then
+ wp.action = 'Turning Point'
+ else
+ WPtype = string.lower(WPtype)
+ if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then
+ wp.action = 'Fly Over Point'
+ elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then
+ wp.action = 'Turning Point'
+ else
+ wp.action = 'Turning Point'
+ end
+ end
+
+ wp.type = 'Turning Point'
+ return wp
+ end
+
+ function mist.heli.buildWP(point, WPtype, speed, alt, altType)
+
+ local wp = {}
+ wp.x = point.x
+
+ if point.z then
+ wp.y = point.z
+ else
+ wp.y = point.y
+ end
+
+ if alt and type(alt) == 'number' then
+ wp.alt = alt
+ else
+ wp.alt = 500
+ end
+
+ if altType then
+ altType = string.lower(altType)
+ if altType == 'radio' or altType == 'agl' then
+ wp.alt_type = 'RADIO'
+ elseif altType == 'baro' or altType == 'asl' then
+ wp.alt_type = 'BARO'
+ end
+ else
+ wp.alt_type = 'RADIO'
+ end
+
+ if point.speed then
+ speed = point.speed
+ end
+
+ if point.type then
+ WPtype = point.type
+ end
+
+ if not speed then
+ wp.speed = mist.utils.kmphToMps(200)
+ else
+ wp.speed = speed
+ end
+
+ if not WPtype then
+ wp.action = 'Turning Point'
+ else
+ WPtype = string.lower(WPtype)
+ if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then
+ wp.action = 'Fly Over Point'
+ elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then
+ wp.action = 'Turning Point'
+ else
+ wp.action = 'Turning Point'
+ end
+ end
+
+ wp.type = 'Turning Point'
+ return wp
+ end
+
+ -- need to return a Vec3 or Vec2?
+ function mist.getRandPointInCircle(point, radius, innerRadius)
+ local theta = 2*math.pi*math.random()
+ local rad = math.random() + math.random()
+ if rad > 1 then
+ rad = 2 - rad
+ end
+
+ local radMult
+ if innerRadius and innerRadius <= radius then
+ radMult = (radius - innerRadius)*rad + innerRadius
+ else
+ radMult = radius*rad
+ end
+
+ if not point.z then --might as well work with vec2/3
+ point.z = point.y
+ end
+
+ local rndCoord
+ if radius > 0 then
+ rndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z}
+ else
+ rndCoord = {x = point.x, y = point.z}
+ end
+ return rndCoord
+ end
+
+ function mist.getRandomPointInZone(zoneName, innerRadius)
+ if type(zoneName) == 'string' and type(trigger.misc.getZone(zoneName)) == 'table' then
+ return mist.getRandPointInCircle(trigger.misc.getZone(zoneName).point, trigger.misc.getZone(zoneName).radius, innerRadius)
+ end
+ return false
+ end
+
+ function mist.groupToRandomPoint(vars)
+ local group = vars.group --Required
+ local point = vars.point --required
+ local radius = vars.radius or 0
+ local innerRadius = vars.innerRadius
+ local form = vars.form or 'Cone'
+ local heading = vars.heading or math.random()*2*math.pi
+ local headingDegrees = vars.headingDegrees
+ local speed = vars.speed or mist.utils.kmphToMps(20)
+
+
+ local useRoads
+ if not vars.disableRoads then
+ useRoads = true
+ else
+ useRoads = false
+ end
+
+ local path = {}
+
+ if headingDegrees then
+ heading = headingDegrees*math.pi/180
+ end
+
+ if heading >= 2*math.pi then
+ heading = heading - 2*math.pi
+ end
+
+ local rndCoord = mist.getRandPointInCircle(point, radius, innerRadius)
+
+ local offset = {}
+ local posStart = mist.getLeadPos(group)
+
+ offset.x = mist.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3)
+ offset.z = mist.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3)
+ path[#path + 1] = mist.ground.buildWP(posStart, form, speed)
+
+
+ if useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then
+ path[#path + 1] = mist.ground.buildWP({x = posStart.x + 11, z = posStart.z + 11}, 'off_road', speed)
+ path[#path + 1] = mist.ground.buildWP(posStart, 'on_road', speed)
+ path[#path + 1] = mist.ground.buildWP(offset, 'on_road', speed)
+ else
+ path[#path + 1] = mist.ground.buildWP({x = posStart.x + 25, z = posStart.z + 25}, form, speed)
+ end
+
+ path[#path + 1] = mist.ground.buildWP(offset, form, speed)
+ path[#path + 1] = mist.ground.buildWP(rndCoord, form, speed)
+
+ mist.goRoute(group, path)
+
+ return
+ end
+
+ function mist.groupRandomDistSelf(gpData, dist, form, heading, speed)
+ local pos = mist.getLeadPos(gpData)
+ local fakeZone = {}
+ fakeZone.radius = dist or math.random(300, 1000)
+ fakeZone.point = {x = pos.x, y = pos.y, z = pos.z}
+ mist.groupToRandomZone(gpData, fakeZone, form, heading, speed)
+
+ return
+ end
+
+ function mist.groupToRandomZone(gpData, zone, form, heading, speed)
+ if type(gpData) == 'string' then
+ gpData = Group.getByName(gpData)
+ end
+
+ if type(zone) == 'string' then
+ zone = trigger.misc.getZone(zone)
+ elseif type(zone) == 'table' and not zone.radius then
+ zone = trigger.misc.getZone(zone[math.random(1, #zone)])
+ end
+
+ if speed then
+ speed = mist.utils.kmphToMps(speed)
+ end
+
+ local vars = {}
+ vars.group = gpData
+ vars.radius = zone.radius
+ vars.form = form
+ vars.headingDegrees = heading
+ vars.speed = speed
+ vars.point = mist.utils.zoneToVec3(zone)
+
+ mist.groupToRandomPoint(vars)
+
+ return
+ end
+
+ function mist.isTerrainValid(coord, terrainTypes) -- vec2/3 and enum or table of acceptable terrain types
+ if coord.z then
+ coord.y = coord.z
+ end
+ local typeConverted = {}
+
+ if type(terrainTypes) == 'string' then -- if its a string it does this check
+ for constId, constData in pairs(land.SurfaceType) do
+ if string.lower(constId) == string.lower(terrainTypes) or string.lower(constData) == string.lower(terrainTypes) then
+ table.insert(typeConverted, constId)
+ end
+ end
+ elseif type(terrainTypes) == 'table' then -- if its a table it does this check
+ for typeId, typeData in pairs(terrainTypes) do
+ for constId, constData in pairs(land.SurfaceType) do
+ if string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeId) then
+ table.insert(typeConverted, constId)
+ end
+ end
+ end
+ end
+ for validIndex, validData in pairs(typeConverted) do
+ if land.getSurfaceType(coord) == land.SurfaceType[validData] then
+ return true
+ end
+ end
+ return false
+ end
+
+ function mist.terrainHeightDiff(coord, searchSize)
+ local samples = {}
+ local searchRadius = 5
+ if searchSize then
+ searchRadius = searchSize
+ end
+ if type(coord) == 'string' then
+ coord = mist.utils.zoneToVec3(coord)
+ end
+
+ coord = mist.utils.makeVec2(coord)
+
+ samples[#samples + 1] = land.getHeight(coord)
+ for i = 0, 360, 30 do
+ samples[#samples + 1] = land.getHeight({x = (coord.x + (math.sin(math.rad(i))*searchRadius)), y = (coord.y + (math.cos(math.rad(i))*searchRadius))})
+ if searchRadius >= 20 then -- if search radius is sorta large, take a sample halfway between center and outer edge
+ samples[#samples + 1] = land.getHeight({x = (coord.x + (math.sin(math.rad(i))*(searchRadius/2))), y = (coord.y + (math.cos(math.rad(i))*(searchRadius/2)))})
+ end
+ end
+ local tMax, tMin = 0, 1000000
+ for index, height in pairs(samples) do
+ if height > tMax then
+ tMax = height
+ end
+ if height < tMin then
+ tMin = height
+ end
+ end
+ return mist.utils.round(tMax - tMin, 2)
+ end
+
+ function mist.groupToPoint(gpData, point, form, heading, speed, useRoads)
+ if type(point) == 'string' then
+ point = trigger.misc.getZone(point)
+ end
+ if speed then
+ speed = mist.utils.kmphToMps(speed)
+ end
+
+ local vars = {}
+ vars.group = gpData
+ vars.form = form
+ vars.headingDegrees = heading
+ vars.speed = speed
+ vars.disableRoads = useRoads
+ vars.point = mist.utils.zoneToVec3(point)
+ mist.groupToRandomPoint(vars)
+
+ return
+ end
+
+ function mist.getLeadPos(group)
+ if type(group) == 'string' then -- group name
+ group = Group.getByName(group)
+ end
+
+ local units = group:getUnits()
+
+ local leader = units[1]
+ if not Unit.isExist(leader) then -- SHOULD be good, but if there is a bug, this code future-proofs it then.
+ local lowestInd = math.huge
+ for ind, unit in pairs(units) do
+ if Unit.isExist(unit) and ind < lowestInd then
+ lowestInd = ind
+ return unit:getPosition().p
+ end
+ end
+ end
+ if leader and Unit.isExist(leader) then -- maybe a little too paranoid now...
+ return leader:getPosition().p
+ end
+ end
+
+end
+
+--- Database tables.
+-- @section mist.DBs
+
+--- Mission data
+-- @table mist.DBs.missionData
+-- @field startTime mission start time
+-- @field theatre mission theatre/map e.g. Caucasus
+-- @field version mission version
+-- @field files mission resources
+
+--- Tables used as parameters.
+-- @section varTables
+
+--- mist.flagFunc.units_in_polygon parameter table.
+-- @table unitsInPolygonVars
+-- @tfield table unit name table @{UnitNameTable}.
+-- @tfield table zone table defining a polygon.
+-- @tfield number|string flag flag to set to true.
+-- @tfield[opt] number|string stopflag if set to true the function
+-- will stop evaluating.
+-- @tfield[opt] number maxalt maximum altitude (MSL) for the
+-- polygon.
+-- @tfield[opt] number req_num minimum number of units that have
+-- to be in the polygon.
+-- @tfield[opt] number interval sets the interval for
+-- checking if units are inside of the polygon in seconds. Default: 1.
+-- @tfield[opt] boolean toggle switch the flag to false if required
+-- conditions are not met. Default: false.
+-- @tfield[opt] table unitTableDef
+
+--- Logger class.
+-- @type mist.Logger
+do -- mist.Logger scope
+ mist.Logger = {}
+
+ --- parses text and substitutes keywords with values from given array.
+ -- @param text string containing keywords to substitute with values
+ -- or a variable.
+ -- @param ... variables to use for substitution in string.
+ -- @treturn string new string with keywords substituted or
+ -- value of variable as string.
+ local function formatText(text, ...)
+ if type(text) ~= 'string' then
+ if type(text) == 'table' then
+ text = mist.utils.oneLineSerialize(text)
+ else
+ text = tostring(text)
+ end
+ else
+ for index,value in ipairs(arg) do
+ -- TODO: check for getmetatabel(value).__tostring
+ if type(value) == 'table' then
+ value = mist.utils.oneLineSerialize(value)
+ else
+ value = tostring(value)
+ end
+ text = text:gsub('$' .. index, value)
+ end
+ end
+ local fName = nil
+ local cLine = nil
+ if debug then
+ local dInfo = debug.getinfo(3)
+ fName = dInfo.name
+ cLine = dInfo.currentline
+ -- local fsrc = dinfo.short_src
+ --local fLine = dInfo.linedefined
+ end
+ if fName and cLine then
+ return fName .. '|' .. cLine .. ': ' .. text
+ elseif cLine then
+ return cLine .. ': ' .. text
+ else
+ return ' ' .. text
+ end
+ end
+
+ local function splitText(text)
+ local tbl = {}
+ while text:len() > 4000 do
+ local sub = text:sub(1, 4000)
+ text = text:sub(4001)
+ table.insert(tbl, sub)
+ end
+ table.insert(tbl, text)
+ return tbl
+ end
+
+ --- Creates a new logger.
+ -- Each logger has it's own tag and log level.
+ -- @tparam string tag tag which appears at the start of
+ -- every log line produced by this logger.
+ -- @tparam[opt] number|string level the log level defines which messages
+ -- will be logged and which will be omitted. Log level 3 beeing the most verbose
+ -- and 0 disabling all output. This can also be a string. Allowed strings are:
+ -- "none" (0), "error" (1), "warning" (2) and "info" (3).
+ -- @usage myLogger = mist.Logger:new("MyScript")
+ -- @usage myLogger = mist.Logger:new("MyScript", 2)
+ -- @usage myLogger = mist.Logger:new("MyScript", "info")
+ -- @treturn mist.Logger
+ function mist.Logger:new(tag, level)
+ local l = {}
+ l.tag = tag
+ setmetatable(l, self)
+ self.__index = self
+ self:setLevel(level)
+ return l
+ end
+
+ --- Sets the level of verbosity for this logger.
+ -- @tparam[opt] number|string level the log level defines which messages
+ -- will be logged and which will be omitted. Log level 3 beeing the most verbose
+ -- and 0 disabling all output. This can also be a string. Allowed strings are:
+ -- "none" (0), "error" (1), "warning" (2) and "info" (3).
+ -- @usage myLogger:setLevel("info")
+ -- @usage -- log everything
+ --myLogger:setLevel(3)
+ function mist.Logger:setLevel(level)
+ if not level then
+ self.level = 2
+ else
+ if type(level) == 'string' then
+ if level == 'none' or level == 'off' then
+ self.level = 0
+ elseif level == 'error' then
+ self.level = 1
+ elseif level == 'warning' or level == 'warn' then
+ self.level = 2
+ elseif level == 'info' then
+ self.level = 3
+ end
+ elseif type(level) == 'number' then
+ self.level = level
+ else
+ self.level = 2
+ end
+ end
+ end
+
+ --- Logs error and shows alert window.
+ -- This logs an error to the dcs.log and shows a popup window,
+ -- pausing the simulation. This works always even if logging is
+ -- disabled by setting a log level of "none" or 0.
+ -- @tparam string text the text with keywords to substitute.
+ -- @param ... variables to be used for substitution.
+ -- @usage myLogger:alert("Shit just hit the fan! WEEEE!!!11")
+ function mist.Logger:alert(text, ...)
+ text = formatText(text, unpack(arg))
+ if text:len() > 4000 then
+ local texts = splitText(text)
+ for i = 1, #texts do
+ if i == 1 then
+ env.error(self.tag .. '|' .. texts[i], true)
+ else
+ env.error(texts[i])
+ end
+ end
+ else
+ env.error(self.tag .. '|' .. text, true)
+ end
+ end
+
+ --- Logs a message, disregarding the log level.
+ -- @tparam string text the text with keywords to substitute.
+ -- @param ... variables to be used for substitution.
+ -- @usage myLogger:msg("Always logged!")
+ function mist.Logger:msg(text, ...)
+ text = formatText(text, unpack(arg))
+ if text:len() > 4000 then
+ local texts = splitText(text)
+ for i = 1, #texts do
+ if i == 1 then
+ env.info(self.tag .. '|' .. texts[i])
+ else
+ env.info(texts[i])
+ end
+ end
+ else
+ env.info(self.tag .. '|' .. text)
+ end
+ end
+
+ --- Logs an error.
+ -- logs a message prefixed with this loggers tag to dcs.log as
+ -- long as at least the "error" log level (1) is set.
+ -- @tparam string text the text with keywords to substitute.
+ -- @param ... variables to be used for substitution.
+ -- @usage myLogger:error("Just an error!")
+ -- @usage myLogger:error("Foo is $1 instead of $2", foo, "bar")
+ function mist.Logger:error(text, ...)
+ if self.level >= 1 then
+ text = formatText(text, unpack(arg))
+ if text:len() > 4000 then
+ local texts = splitText(text)
+ for i = 1, #texts do
+ if i == 1 then
+ env.error(self.tag .. '|' .. texts[i])
+ else
+ env.error(texts[i])
+ end
+ end
+ else
+ env.error(self.tag .. '|' .. text)
+ end
+ end
+ end
+
+ --- Logs a warning.
+ -- logs a message prefixed with this loggers tag to dcs.log as
+ -- long as at least the "warning" log level (2) is set.
+ -- @tparam string text the text with keywords to substitute.
+ -- @param ... variables to be used for substitution.
+ -- @usage myLogger:warn("Mother warned you! Those $1 from the interwebs are $2", {"geeks", 1337})
+ function mist.Logger:warn(text, ...)
+ if self.level >= 2 then
+ text = formatText(text, unpack(arg))
+ if text:len() > 4000 then
+ local texts = splitText(text)
+ for i = 1, #texts do
+ if i == 1 then
+ env.warning(self.tag .. '|' .. texts[i])
+ else
+ env.warning(texts[i])
+ end
+ end
+ else
+ env.warning(self.tag .. '|' .. text)
+ end
+ end
+ end
+
+ --- Logs a info.
+ -- logs a message prefixed with this loggers tag to dcs.log as
+ -- long as the highest log level (3) "info" is set.
+ -- @tparam string text the text with keywords to substitute.
+ -- @param ... variables to be used for substitution.
+ -- @see warn
+ function mist.Logger:info(text, ...)
+ if self.level >= 3 then
+ text = formatText(text, unpack(arg))
+ if text:len() > 4000 then
+ local texts = splitText(text)
+ for i = 1, #texts do
+ if i == 1 then
+ env.info(self.tag .. '|' .. texts[i])
+ else
+ env.info(texts[i])
+ end
+ end
+ else
+ env.info(self.tag .. '|' .. text)
+ end
+ end
+ end
+
+end
+
+-- initialize mist
+mist.init()
+env.info(('Mist version ' .. mist.majorVersion .. '.' .. mist.minorVersion .. '.' .. mist.build .. ' loaded.'))
+
+-- vim: noet:ts=2:sw=2
diff --git a/resources/plugins/jtacautolase/plugin.json b/resources/plugins/jtacautolase/plugin.json
new file mode 100644
index 00000000..33ce09dd
--- /dev/null
+++ b/resources/plugins/jtacautolase/plugin.json
@@ -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"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/resources/plugins/plugins.json b/resources/plugins/plugins.json
new file mode 100644
index 00000000..21b44606
--- /dev/null
+++ b/resources/plugins/plugins.json
@@ -0,0 +1,5 @@
+[
+ "veaf",
+ "jtacautolase",
+ "base"
+]
diff --git a/resources/scripts/plugins/__plugins.lst.sample b/resources/scripts/plugins/__plugins.lst.sample
deleted file mode 100644
index 27cbf0c0..00000000
--- a/resources/scripts/plugins/__plugins.lst.sample
+++ /dev/null
@@ -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
diff --git a/resources/ui/misc/light/plugins.png b/resources/ui/misc/light/plugins.png
new file mode 100644
index 00000000..568d9e95
Binary files /dev/null and b/resources/ui/misc/light/plugins.png differ
diff --git a/resources/ui/misc/light/pluginsoptions.png b/resources/ui/misc/light/pluginsoptions.png
new file mode 100644
index 00000000..b9090ea2
Binary files /dev/null and b/resources/ui/misc/light/pluginsoptions.png differ