From 2c1dc6a18d78f8348f015829830bc7c0a675847b Mon Sep 17 00:00:00 2001 From: David Pierron Date: Wed, 30 Sep 2020 13:34:41 +0200 Subject: [PATCH 01/10] removed useless link.cmd.sample file --- resources/scripts/plugins/link.cmd.sample | 29 ----------------------- 1 file changed, 29 deletions(-) delete mode 100644 resources/scripts/plugins/link.cmd.sample diff --git a/resources/scripts/plugins/link.cmd.sample b/resources/scripts/plugins/link.cmd.sample deleted file mode 100644 index e9c69ce7..00000000 --- a/resources/scripts/plugins/link.cmd.sample +++ /dev/null @@ -1,29 +0,0 @@ -rem this can be used to easily create hardlinks from your plugin development folder - -mklink mist.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\community\mist.lua -mklink Moose.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\community\Moose.lua -mklink CTLD.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\community\CTLD.lua -mklink NIOD.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\community\NIOD.lua -mklink WeatherMark.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\community\WeatherMark.lua -mklink veaf.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veaf.lua -mklink dcsUnits.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\dcsUnits.lua -mklink JTACAutoLase.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\JTACAutoLase.lua -mklink veafAssets.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafAssets.lua -mklink veafCarrierOperations.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafCarrierOperations.lua -mklink veafCarrierOperations2.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafCarrierOperations2.lua -mklink veafCasMission.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafCasMission.lua -mklink veafCombatMission.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafCombatMission.lua -mklink veafCombatZone.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafCombatZone.lua -mklink veafGrass.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafGrass.lua -mklink veafInterpreter.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafInterpreter.lua -mklink veafMarkers.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafMarkers.lua -mklink veafMove.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafMove.lua -mklink veafNamedPoints.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafNamedPoints.lua -mklink veafRadio.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafRadio.lua -mklink veafRemote.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafRemote.lua -mklink veafSecurity.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafSecurity.lua -mklink veafShortcuts.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafShortcuts.lua -mklink veafSpawn.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafSpawn.lua -mklink veafTransportMission.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafTransportMission.lua -mklink veafUnits.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafUnits.lua -mklink missionConfig.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\missionConfig.lua From 5807fbf89640a62addfc6c6445aa5a800bc3ca1f Mon Sep 17 00:00:00 2001 From: David Pierron Date: Wed, 30 Sep 2020 13:35:09 +0200 Subject: [PATCH 02/10] added a 'mkrelease' config for VS.Code --- .vscode/launch.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index 646c8768..fde0564f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,6 +14,17 @@ "PYTHONPATH": ".;./pydcs" }, "preLaunchTask": "Prepare Environment" + }, + { + "name": "Python: Make Release", + "type": "python", + "request": "launch", + "program": "resources\\tools\\mkrelease.py", + "console": "integratedTerminal", + "env": { + "PYTHONPATH": ".;./pydcs" + }, + "preLaunchTask": "Prepare Environment" } ] } \ No newline at end of file From c77bfe9da21408119754411eaf5eaff14f5af3ea Mon Sep 17 00:00:00 2001 From: David Pierron Date: Fri, 9 Oct 2020 21:25:21 +0200 Subject: [PATCH 03/10] plugin base : inject mission configuration data --- game/operation/operation.py | 208 ++++++++++++++++++++++++++----- gen/aircraft.py | 9 +- gen/airsupportgen.py | 6 +- gen/armor.py | 3 +- gen/flights/ai_flight_planner.py | 3 +- gen/flights/flight.py | 1 + 6 files changed, 192 insertions(+), 38 deletions(-) diff --git a/game/operation/operation.py b/game/operation/operation.py index ecc82e51..66eddb78 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -14,7 +14,7 @@ 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 @@ -259,6 +259,181 @@ class Operation: if self.game.settings.perf_smoke_gen: visualgen.generate() + luaData = {} + luaData["AircraftCarriers"] = {} + luaData["Tankers"] = {} + luaData["AWACs"] = {} + luaData["JTACs"] = {} + luaData["TargetPoints"] = {} + + self.assign_channels_to_flights(airgen.flights, + airsupportgen.air_support) + + 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() + + + # 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 +""" + # 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 Plugins Lua Scripts listOfPluginsScripts = [] plugin_file_path = Path("./resources/scripts/plugins/__plugins.lst") @@ -296,37 +471,6 @@ class Operation: 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 = {} - """ - - 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") diff --git a/gen/aircraft.py b/gen/aircraft.py index cbb028fe..08491d42 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -238,11 +238,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 @@ -255,6 +258,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]: @@ -779,7 +783,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 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..b6133b6b 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -54,6 +54,7 @@ RANDOM_OFFSET_ATTACK = 250 @dataclass(frozen=True) class JtacInfo: """JTAC information.""" + dcsGroupName: str unit_name: str callsign: str region: str @@ -158,7 +159,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/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 09be3773..561f359c 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -205,6 +205,7 @@ class PackageBuilder: airfield, aircraft = assignment flight = Flight(aircraft, plan.num_aircraft, airfield, plan.task) self.package.add_flight(flight) + flight.targetPoint = location return True def build(self) -> Package: @@ -217,7 +218,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.""" diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 0c972723..cef0987d 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -132,6 +132,7 @@ class Flight: preset_loadout_name = "" start_type = "Runway" group = False # Contains DCS Mission group data after mission has been generated + targetPoint = None # Contains either None or a Strike/SEAD target point location # How long before this flight should take off scheduled_in = 0 From d22943d7557efbcdcc62f0609298b59ef3db687c Mon Sep 17 00:00:00 2001 From: David Pierron Date: Mon, 12 Oct 2020 17:27:13 +0200 Subject: [PATCH 04/10] added a customizable plugin system - the base LUA functionality has been implemented as a mandatory plugin - the jtacautolase functionality has been implemented as a plugin - added a VEAF framework plugin The plugins have GUI elements in the Settings window. --- .gitignore | 3 +- .gitmodules | 3 + game/operation/operation.py | 98 +++++++----------- game/settings.py | 3 + gen/flights/ai_flight_planner.py | 4 +- plugin/__init__.py | 10 ++ .../base}/dcs_liberation.lua | 0 {resources/scripts => plugin/base}/json.lua | 0 .../scripts => plugin/base}/mist_4_3_74.lua | 0 plugin/base_plugin.py | 41 ++++++++ .../custom}/__plugins.lst.sample | 0 .../jtacautolase}/JTACAutoLase.lua | 0 plugin/jtacautolase_plugin.py | 80 ++++++++++++++ plugin/liberation_plugin.py | 20 ++++ plugin/veaf | 1 + plugin/veaf_plugin.py | 90 ++++++++++++++++ qt_ui/uiconstants.py | 2 + qt_ui/windows/settings/QSettingsWindow.py | 40 ++++++- resources/ui/misc/light/plugins.png | Bin 0 -> 1268 bytes resources/ui/misc/light/pluginsoptions.png | Bin 0 -> 1345 bytes 20 files changed, 330 insertions(+), 65 deletions(-) create mode 100644 plugin/__init__.py rename {resources/scripts => plugin/base}/dcs_liberation.lua (100%) rename {resources/scripts => plugin/base}/json.lua (100%) rename {resources/scripts => plugin/base}/mist_4_3_74.lua (100%) create mode 100644 plugin/base_plugin.py rename {resources/scripts/plugins => plugin/custom}/__plugins.lst.sample (100%) rename {resources/scripts => plugin/jtacautolase}/JTACAutoLase.lua (100%) create mode 100644 plugin/jtacautolase_plugin.py create mode 100644 plugin/liberation_plugin.py create mode 160000 plugin/veaf create mode 100644 plugin/veaf_plugin.py create mode 100644 resources/ui/misc/light/plugins.png create mode 100644 resources/ui/misc/light/pluginsoptions.png diff --git a/.gitignore b/.gitignore index 1bf595f6..f058e01f 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ logs/ qt_ui/logs/liberation.log *.psd -resources/scripts/plugins/* +plugin/custom/__plugins.lst +plugin/custom/*.lua diff --git a/.gitmodules b/.gitmodules index d8db9cf5..e4041d5d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ path = pydcs url = https://github.com/pydcs/dcs branch = master +[submodule "plugin/veaf"] + path = plugin/veaf + url = https://github.com/VEAF/dcs-liberation-veaf-framework diff --git a/game/operation/operation.py b/game/operation/operation.py index 66eddb78..b537395e 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -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 BasePlugin, INSTALLED_PLUGINS class Operation: attackers_starting_position = None # type: db.StartingPosition @@ -75,6 +75,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 +134,36 @@ 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) + + if pluginName == None: + pluginName = "custom" + plugin_path = Path("./plugin",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() @@ -434,67 +465,12 @@ dcsLiberation.TargetPoints = { trigger.add_action(DoScript(String(lua))) self.current_mission.triggerrules.triggers.append(trigger) - # 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") + # Inject Plugins Lua Scripts and data + self.listOfPluginsScripts = [] - # 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) - - # 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) - - # 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) - - # 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) - - # 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 INSTALLED_PLUGINS: + 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..5d0d5c91 100644 --- a/game/settings.py +++ b/game/settings.py @@ -40,4 +40,7 @@ class Settings: self.perf_culling = False self.perf_culling_distance = 100 + # LUA Plugins system + self.plugins = {} + diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 561f359c..03fe6d32 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -205,7 +205,7 @@ class PackageBuilder: airfield, aircraft = assignment flight = Flight(aircraft, plan.num_aircraft, airfield, plan.task) self.package.add_flight(flight) - flight.targetPoint = location + flight.targetPoint = self.package.target return True def build(self) -> Package: @@ -218,7 +218,7 @@ class PackageBuilder: for flight in flights: self.global_inventory.return_from_flight(flight) self.package.remove_flight(flight) - flight.targetPoint = None + flight.targetPoint = None class ObjectiveFinder: """Identifies potential objectives for the mission planner.""" diff --git a/plugin/__init__.py b/plugin/__init__.py new file mode 100644 index 00000000..c3cd6fb2 --- /dev/null +++ b/plugin/__init__.py @@ -0,0 +1,10 @@ +from .base_plugin import BasePlugin +from .veaf_plugin import VeafPlugin +from .jtacautolase_plugin import JtacAutolasePlugin +from .liberation_plugin import LiberationPlugin + +INSTALLED_PLUGINS=[ + VeafPlugin(), + JtacAutolasePlugin(), + LiberationPlugin() + ] \ No newline at end of file diff --git a/resources/scripts/dcs_liberation.lua b/plugin/base/dcs_liberation.lua similarity index 100% rename from resources/scripts/dcs_liberation.lua rename to plugin/base/dcs_liberation.lua diff --git a/resources/scripts/json.lua b/plugin/base/json.lua similarity index 100% rename from resources/scripts/json.lua rename to plugin/base/json.lua diff --git a/resources/scripts/mist_4_3_74.lua b/plugin/base/mist_4_3_74.lua similarity index 100% rename from resources/scripts/mist_4_3_74.lua rename to plugin/base/mist_4_3_74.lua diff --git a/plugin/base_plugin.py b/plugin/base_plugin.py new file mode 100644 index 00000000..c2a850e2 --- /dev/null +++ b/plugin/base_plugin.py @@ -0,0 +1,41 @@ +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 + +class BasePlugin(): + nameInUI:str = "Base plugin" + nameInSettings:str = "plugin.base" + enabledDefaultValue:bool = False + + def __init__(self): + self.uiWidget: QCheckBox = None + self.enabled = self.enabledDefaultValue + self.settings = None + + def setupUI(self, settingsWindow, row:int): + self.settings = settingsWindow.game.settings + + if not self.nameInSettings in self.settings.plugins: + self.settings.plugins[self.nameInSettings] = self.enabledDefaultValue + + self.uiWidget = QCheckBox() + self.uiWidget.setChecked(self.settings.plugins[self.nameInSettings]) + 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) + + def applySetting(self, settingsWindow): + self.settings.plugins[self.nameInSettings] = self.uiWidget.isChecked() + self.enabled = self.settings.plugins[self.nameInSettings] + + def injectScripts(self, operation): + self.settings = operation.game.settings + return self.isEnabled() + + def injectConfiguration(self, operation): + self.settings = operation.game.settings + return self.isEnabled() + + def isEnabled(self) -> bool: + return self.settings != None and self.settings.plugins[self.nameInSettings] diff --git a/resources/scripts/plugins/__plugins.lst.sample b/plugin/custom/__plugins.lst.sample similarity index 100% rename from resources/scripts/plugins/__plugins.lst.sample rename to plugin/custom/__plugins.lst.sample diff --git a/resources/scripts/JTACAutoLase.lua b/plugin/jtacautolase/JTACAutoLase.lua similarity index 100% rename from resources/scripts/JTACAutoLase.lua rename to plugin/jtacautolase/JTACAutoLase.lua diff --git a/plugin/jtacautolase_plugin.py b/plugin/jtacautolase_plugin.py new file mode 100644 index 00000000..d8becf03 --- /dev/null +++ b/plugin/jtacautolase_plugin.py @@ -0,0 +1,80 @@ +from dcs.triggers import TriggerStart +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 +from .base_plugin import BasePlugin + +class JtacAutolasePlugin(BasePlugin): + nameInUI:str = "JTAC Autolase" + nameInSettings:str = "plugin.jtacAutolase" + enabledDefaultValue:bool = True + + #Allow spawn option + nameInUI_useSmoke:str = "JTACs use smoke" + nameInSettings_useSmoke:str = "plugin.jtacAutolase.useSmoke" + + def setupUI(self, settingsWindow, row:int): + # call the base method to add the plugin selection checkbox + super().setupUI(settingsWindow, row) + + if settingsWindow.pluginsOptionsPageLayout: + self.optionsGroup = QGroupBox(self.nameInUI) + optionsGroupLayout = QGridLayout(); + optionsGroupLayout.setAlignment(Qt.AlignTop) + self.optionsGroup.setLayout(optionsGroupLayout) + settingsWindow.pluginsOptionsPageLayout.addWidget(self.optionsGroup) + + # JTAC use smoke + if not self.nameInSettings_useSmoke in self.settings.plugins: + self.settings.plugins[self.nameInSettings_useSmoke] = True + + self.uiWidget_useSmoke = QCheckBox() + self.uiWidget_useSmoke.setChecked(self.settings.plugins[self.nameInSettings_useSmoke]) + self.uiWidget_useSmoke.toggled.connect(lambda: self.applySetting(settingsWindow)) + + optionsGroupLayout.addWidget(QLabel(self.nameInUI_useSmoke), 0, 0) + optionsGroupLayout.addWidget(self.uiWidget_useSmoke, 0, 1, Qt.AlignRight) + + # disable or enable the UI in the plugins special page + self.enableOptionsGroup() + + def enableOptionsGroup(self): + pluginEnabled = self.uiWidget.isChecked() + self.optionsGroup.setEnabled(pluginEnabled) + + def applySetting(self, settingsWindow): + # call the base method to apply the plugin selection checkbox value + super().applySetting(settingsWindow) + + # save the "use smoke" option + self.settings.plugins[self.nameInSettings_useSmoke] = self.uiWidget_useSmoke.isChecked() + + # disable or enable the UI in the plugins special page + self.enableOptionsGroup() + + def injectScripts(self, operation): + if super().injectScripts(operation): + operation.injectPluginScript("jtacautolase", "JTACAutoLase.lua", "jtacautolase") + + def injectConfiguration(self, operation): + if super().injectConfiguration(operation): + + # add a configuration for JTACAutoLase and start lasing for all JTACs + smoke = "local smoke = false" + if self.settings.plugins[self.nameInSettings_useSmoke]: + smoke = "local smoke = true" + + lua = smoke + """ + + -- setting and starting JTACs + env.info("DCSLiberation|: setting and starting JTACs") + + for _, jtac in pairs(dcsLiberation.JTACs) do + if dcsLiberation.JTACAutoLase then + dcsLiberation.JTACAutoLase(jtac.dcsUnit, jtac.code, smoke, 'vehicle') + end + end + """ + + operation.injectLuaTrigger(lua, "Setting and starting JTACs") + diff --git a/plugin/liberation_plugin.py b/plugin/liberation_plugin.py new file mode 100644 index 00000000..e9a01a39 --- /dev/null +++ b/plugin/liberation_plugin.py @@ -0,0 +1,20 @@ +from .base_plugin import BasePlugin + +class LiberationPlugin(BasePlugin): + nameInUI:str = "Liberation script" + nameInSettings:str = "plugin.liberation" + enabledDefaultValue:bool = True + + def setupUI(self, settingsWindow, row:int): + # Don't setup any UI, this plugin is mandatory + pass + + def injectScripts(self, operation): + if super().injectScripts(operation): + operation.injectPluginScript("base", "mist_4_3_74.lua", "mist") + operation.injectPluginScript("base", "json.lua", "json") + operation.injectPluginScript("base", "dcs_liberation.lua", "liberation") + + def injectConfiguration(self, operation): + if super().injectConfiguration(operation): + pass diff --git a/plugin/veaf b/plugin/veaf new file mode 160000 index 00000000..219cdffe --- /dev/null +++ b/plugin/veaf @@ -0,0 +1 @@ +Subproject commit 219cdffef087660fe448a41e1f187c4856e9d80f diff --git a/plugin/veaf_plugin.py b/plugin/veaf_plugin.py new file mode 100644 index 00000000..2e3b0531 --- /dev/null +++ b/plugin/veaf_plugin.py @@ -0,0 +1,90 @@ +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 +from .base_plugin import BasePlugin + +class VeafPlugin(BasePlugin): + nameInUI:str = "VEAF framework" + nameInSettings:str = "plugin.veaf" + enabledDefaultValue:bool = False + + #Allow spawn option + nameInUI_allowSpawn:str = "Allow units spawn via markers and CTLD (not implemented yet)" + nameInSettings_allowSpawn:str = "plugin.veaf.allowSpawn" + + def setupUI(self, settingsWindow, row:int): + # call the base method to add the plugin selection checkbox + super().setupUI(settingsWindow, row) + + if settingsWindow.pluginsOptionsPageLayout: + self.optionsGroup = QGroupBox(self.nameInUI) + optionsGroupLayout = QGridLayout(); + optionsGroupLayout.setAlignment(Qt.AlignTop) + self.optionsGroup.setLayout(optionsGroupLayout) + settingsWindow.pluginsOptionsPageLayout.addWidget(self.optionsGroup) + + # allow spawn of objects + if not self.nameInSettings_allowSpawn in self.settings.plugins: + self.settings.plugins[self.nameInSettings_allowSpawn] = True + + self.uiWidget_allowSpawn = QCheckBox() + self.uiWidget_allowSpawn.setChecked(self.settings.plugins[self.nameInSettings_allowSpawn]) + self.uiWidget_allowSpawn.setEnabled(False) + self.uiWidget_allowSpawn.toggled.connect(lambda: self.applySetting(settingsWindow)) + + optionsGroupLayout.addWidget(QLabel(self.nameInUI_allowSpawn), 0, 0) + optionsGroupLayout.addWidget(self.uiWidget_allowSpawn, 0, 1, Qt.AlignRight) + + # disable or enable the UI in the plugins special page + self.enableOptionsGroup() + + def enableOptionsGroup(self): + pluginEnabled = self.uiWidget.isChecked() + self.optionsGroup.setEnabled(pluginEnabled) + + def applySetting(self, settingsWindow): + # call the base method to apply the plugin selection checkbox value + super().applySetting(settingsWindow) + + # save the "allow spawn" option + self.settings.plugins[self.nameInSettings_allowSpawn] = self.uiWidget_allowSpawn.isChecked() + + # disable or enable the UI in the plugins special page + self.enableOptionsGroup() + + def injectScripts(self, operation): + if super().injectScripts(operation): + # bypass JTACAutoLase + operation.bypassPluginScript("veaf", "jtacautolase") + + # inject the required scripts + operation.injectPluginScript("veaf", "src\\scripts\\mist.lua", "mist") + operation.injectPluginScript("veaf", "src\\scripts\\Moose.lua", "moose") + operation.injectPluginScript("veaf", "src\\scripts\\CTLD.lua", "ctld") + operation.injectPluginScript("veaf", "src\\scripts\\NIOD.lua", "niod") + operation.injectPluginScript("veaf", "src\\scripts\\WeatherMark.lua", "weathermark") + operation.injectPluginScript("veaf", "src\\scripts\\veaf.lua", "veaf") + operation.injectPluginScript("veaf", "src\\scripts\\dcsUnits.lua", "dcsunits") + operation.injectPluginScript("veaf", "src\\scripts\\veafAssets.lua", "veafassets") + operation.injectPluginScript("veaf", "src\\scripts\\veafCarrierOperations.lua", "veafcarrieroperations") + operation.injectPluginScript("veaf", "src\\scripts\\veafCasMission.lua", "veafcasmission") + operation.injectPluginScript("veaf", "src\\scripts\\veafCombatMission.lua", "veafcombatmission") + operation.injectPluginScript("veaf", "src\\scripts\\veafCombatZone.lua", "veafcombatzone") + operation.injectPluginScript("veaf", "src\\scripts\\veafGrass.lua", "veafgrass") + operation.injectPluginScript("veaf", "src\\scripts\\veafInterpreter.lua", "veafinterpreter") + operation.injectPluginScript("veaf", "src\\scripts\\veafMarkers.lua", "veafmarkers") + operation.injectPluginScript("veaf", "src\\scripts\\veafMove.lua", "veafmove") + operation.injectPluginScript("veaf", "src\\scripts\\veafNamedPoints.lua", "veafnamedpoints") + operation.injectPluginScript("veaf", "src\\scripts\\veafRadio.lua", "veafradio") + operation.injectPluginScript("veaf", "src\\scripts\\veafRemote.lua", "veafremote") + operation.injectPluginScript("veaf", "src\\scripts\\veafSecurity.lua", "veafsecurity") + operation.injectPluginScript("veaf", "src\\scripts\\veafShortcuts.lua", "veafshortcuts") + operation.injectPluginScript("veaf", "src\\scripts\\veafSpawn.lua", "veafspawn") + operation.injectPluginScript("veaf", "src\\scripts\\veafTransportMission.lua", "veaftransportmission") + operation.injectPluginScript("veaf", "src\\scripts\\veafUnits.lua", "veafunits") + + + def injectConfiguration(self, operation): + if super().injectConfiguration(operation): + operation.injectPluginScript("veaf", "src\\config\\missionConfig.lua", "missionconfig") + diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py index 5c97dc72..5c831c1e 100644 --- a/qt_ui/uiconstants.py +++ b/qt_ui/uiconstants.py @@ -99,6 +99,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/windows/settings/QSettingsWindow.py b/qt_ui/windows/settings/QSettingsWindow.py index 6ccdb226..82010be9 100644 --- a/qt_ui/windows/settings/QSettingsWindow.py +++ b/qt_ui/windows/settings/QSettingsWindow.py @@ -11,7 +11,7 @@ from game.game import Game from game.infos.information import Information from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.finances.QFinancesMenu import QHorizontalSeparationLine - +from plugin import BasePlugin, INSTALLED_PLUGINS class QSettingsWindow(QDialog): @@ -52,10 +52,22 @@ class QSettingsWindow(QDialog): cheat.setEditable(False) cheat.setSelectable(True) + plugins = QStandardItem("LUA Plugins") + plugins.setIcon(CONST.ICONS["Plugins"]) + plugins.setEditable(False) + plugins.setSelectable(True) + + pluginsOptions = QStandardItem("LUA Plugins Options") + pluginsOptions.setIcon(CONST.ICONS["PluginsOptions"]) + pluginsOptions.setEditable(False) + pluginsOptions.setSelectable(True) + self.categoryList.setIconSize(QSize(32, 32)) self.categoryModel.appendRow(difficulty) self.categoryModel.appendRow(generator) self.categoryModel.appendRow(cheat) + self.categoryModel.appendRow(plugins) + self.categoryModel.appendRow(pluginsOptions) self.categoryList.setSelectionBehavior(QAbstractItemView.SelectRows) self.categoryList.setModel(self.categoryModel) @@ -65,10 +77,13 @@ class QSettingsWindow(QDialog): self.initDifficultyLayout() self.initGeneratorLayout() self.initCheatLayout() + self.initPluginsLayout() self.right_layout.addWidget(self.difficultyPage) self.right_layout.addWidget(self.generatorPage) self.right_layout.addWidget(self.cheatPage) + self.right_layout.addWidget(self.pluginsPage) + self.right_layout.addWidget(self.pluginsOptionsPage) self.layout.addWidget(self.categoryList, 0, 0, 1, 1) self.layout.addLayout(self.right_layout, 0, 1, 5, 1) @@ -283,6 +298,29 @@ class QSettingsWindow(QDialog): self.moneyCheatBoxLayout.addWidget(btn, i/2, i%2) self.cheatLayout.addWidget(self.moneyCheatBox, 0, 0) + def initPluginsLayout(self): + 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) + + row:int = 0 + for plugin in INSTALLED_PLUGINS: + plugin.setupUI(self, row) + row = row + 1 + + self.pluginsPageLayout.addWidget(self.pluginsGroup) + def cheatLambda(self, amount): return lambda: self.cheatMoney(amount) diff --git a/resources/ui/misc/light/plugins.png b/resources/ui/misc/light/plugins.png new file mode 100644 index 0000000000000000000000000000000000000000..568d9e9581ed36753a5cfc9c0f431eb8c9aee38f GIT binary patch literal 1268 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 Date: Mon, 12 Oct 2020 19:49:39 +0200 Subject: [PATCH 05/10] multiple changes - load plugins when loading a game - moved plugins scripts to resources/plugins (for pyinstaller) - removed vanilla JTAC and JTAC_smoke options and settings GUI - call JtacAutolasePlugin in armor.py - made a dictionary of INSTALLED_PLUGINS - removed NIOD from the VEAF plugin --- .gitignore | 4 +-- .gitmodules | 3 +++ game/game.py | 6 +++++ game/operation/operation.py | 6 +++-- game/settings.py | 7 ++++-- gen/armor.py | 4 ++- plugin/__init__.py | 10 ++++---- plugin/base_plugin.py | 15 +++++++++--- plugin/jtacautolase_plugin.py | 17 ++++++++++++- plugin/liberation_plugin.py | 3 +++ plugin/veaf_plugin.py | 2 +- qt_ui/windows/settings/QSettingsWindow.py | 23 ++---------------- .../plugins}/base/dcs_liberation.lua | 0 {plugin => resources/plugins}/base/json.lua | 0 .../plugins}/base/mist_4_3_74.lua | 0 .../plugins}/custom/__plugins.lst.sample | 0 resources/plugins/doc/0.png | Bin 0 -> 25178 bytes resources/plugins/doc/1.png | Bin 0 -> 26777 bytes resources/plugins/doc/2.png | Bin 0 -> 19406 bytes .../plugins}/jtacautolase/JTACAutoLase.lua | 0 {plugin => resources/plugins}/veaf | 0 21 files changed, 61 insertions(+), 39 deletions(-) rename {plugin => resources/plugins}/base/dcs_liberation.lua (100%) rename {plugin => resources/plugins}/base/json.lua (100%) rename {plugin => resources/plugins}/base/mist_4_3_74.lua (100%) rename {plugin => resources/plugins}/custom/__plugins.lst.sample (100%) create mode 100644 resources/plugins/doc/0.png create mode 100644 resources/plugins/doc/1.png create mode 100644 resources/plugins/doc/2.png rename {plugin => resources/plugins}/jtacautolase/JTACAutoLase.lua (100%) rename {plugin => resources/plugins}/veaf (100%) diff --git a/.gitignore b/.gitignore index f058e01f..ec279d3d 100644 --- a/.gitignore +++ b/.gitignore @@ -21,5 +21,5 @@ logs/ qt_ui/logs/liberation.log *.psd -plugin/custom/__plugins.lst -plugin/custom/*.lua +resources/plugins/custom/__plugins.lst +resources/plugins/custom/*.lua diff --git a/.gitmodules b/.gitmodules index e4041d5d..4d2ad31a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,3 +5,6 @@ [submodule "plugin/veaf"] path = plugin/veaf url = https://github.com/VEAF/dcs-liberation-veaf-framework +[submodule "resources/plugins/veaf"] + path = resources/plugins/veaf + url = https://github.com/VEAF/dcs-liberation-veaf-framework diff --git a/game/game.py b/game/game.py index 57fabfd6..67eeb200 100644 --- a/game/game.py +++ b/game/game.py @@ -28,6 +28,7 @@ from .event.event import Event, UnitsDeliveryEvent from .event.frontlineattack import FrontlineAttackEvent from .infos.information import Information from .settings import Settings +from plugin import INSTALLED_PLUGINS COMMISION_UNIT_VARIETY = 4 COMMISION_LIMITS_SCALE = 1.5 @@ -217,6 +218,11 @@ class Game: def on_load(self) -> None: ObjectiveDistanceCache.set_theater(self.theater) + + # set the settings in all plugins + for pluginName in INSTALLED_PLUGINS: + plugin = INSTALLED_PLUGINS[pluginName] + plugin.setSettings(self.settings) def pass_turn(self, no_action=False): logging.info("Pass turn") diff --git a/game/operation/operation.py b/game/operation/operation.py index b537395e..e42d7b14 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -148,7 +148,8 @@ class Operation: if pluginName == None: pluginName = "custom" - plugin_path = Path("./plugin",pluginName) + plugin_path = Path("./resources/plugins",pluginName) + logging.debug(f"plugin_path = {plugin_path}") if scriptFile != None: scriptFile_path = Path(plugin_path, scriptFile) @@ -468,7 +469,8 @@ dcsLiberation.TargetPoints = { # Inject Plugins Lua Scripts and data self.listOfPluginsScripts = [] - for plugin in INSTALLED_PLUGINS: + for pluginName in INSTALLED_PLUGINS: + plugin = INSTALLED_PLUGINS[pluginName] plugin.injectScripts(self) plugin.injectConfiguration(self) diff --git a/game/settings.py b/game/settings.py index 5d0d5c91..bb7754d4 100644 --- a/game/settings.py +++ b/game/settings.py @@ -1,3 +1,4 @@ +from plugin import INSTALLED_PLUGINS 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 @@ -42,5 +41,9 @@ class Settings: # LUA Plugins system self.plugins = {} + for pluginName in INSTALLED_PLUGINS: + plugin = INSTALLED_PLUGINS[pluginName] + plugin.setSettings(self) + diff --git a/gen/armor.py b/gen/armor.py index b6133b6b..21ae5a25 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 INSTALLED_PLUGINS SPREAD_DISTANCE_FACTOR = 0.1, 0.3 SPREAD_DISTANCE_SIZE_FACTOR = 0.1 @@ -139,7 +140,8 @@ 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: + useJTAC = INSTALLED_PLUGINS and INSTALLED_PLUGINS["JtacAutolasePlugin"] and INSTALLED_PLUGINS["JtacAutolasePlugin"].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) diff --git a/plugin/__init__.py b/plugin/__init__.py index c3cd6fb2..2bbf459c 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -3,8 +3,8 @@ from .veaf_plugin import VeafPlugin from .jtacautolase_plugin import JtacAutolasePlugin from .liberation_plugin import LiberationPlugin -INSTALLED_PLUGINS=[ - VeafPlugin(), - JtacAutolasePlugin(), - LiberationPlugin() - ] \ No newline at end of file +INSTALLED_PLUGINS={ + "VeafPlugin": VeafPlugin(), + "JtacAutolasePlugin": JtacAutolasePlugin(), + "LiberationPlugin": LiberationPlugin(), +} diff --git a/plugin/base_plugin.py b/plugin/base_plugin.py index c2a850e2..4eb6e744 100644 --- a/plugin/base_plugin.py +++ b/plugin/base_plugin.py @@ -15,16 +15,18 @@ class BasePlugin(): def setupUI(self, settingsWindow, row:int): self.settings = settingsWindow.game.settings - if not self.nameInSettings in self.settings.plugins: - self.settings.plugins[self.nameInSettings] = self.enabledDefaultValue - self.uiWidget = QCheckBox() - self.uiWidget.setChecked(self.settings.plugins[self.nameInSettings]) + 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) + def setSettings(self, settings): + self.settings = settings + if not self.nameInSettings in self.settings.plugins: + self.settings.plugins[self.nameInSettings] = self.enabledDefaultValue + def applySetting(self, settingsWindow): self.settings.plugins[self.nameInSettings] = self.uiWidget.isChecked() self.enabled = self.settings.plugins[self.nameInSettings] @@ -38,4 +40,9 @@ class BasePlugin(): 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] diff --git a/plugin/jtacautolase_plugin.py b/plugin/jtacautolase_plugin.py index d8becf03..8b0c316c 100644 --- a/plugin/jtacautolase_plugin.py +++ b/plugin/jtacautolase_plugin.py @@ -12,6 +12,7 @@ class JtacAutolasePlugin(BasePlugin): #Allow spawn option nameInUI_useSmoke:str = "JTACs use smoke" nameInSettings_useSmoke:str = "plugin.jtacAutolase.useSmoke" + enabledDefaultValue_useSmoke:bool = True def setupUI(self, settingsWindow, row:int): # call the base method to add the plugin selection checkbox @@ -42,6 +43,12 @@ class JtacAutolasePlugin(BasePlugin): pluginEnabled = self.uiWidget.isChecked() self.optionsGroup.setEnabled(pluginEnabled) + def setSettings(self, settings): + # call the base method + super().setSettings(settings) + if not self.nameInSettings_useSmoke in self.settings.plugins: + self.settings.plugins[self.nameInSettings_useSmoke] = self.enabledDefaultValue_useSmoke + def applySetting(self, settingsWindow): # call the base method to apply the plugin selection checkbox value super().applySetting(settingsWindow) @@ -61,7 +68,7 @@ class JtacAutolasePlugin(BasePlugin): # add a configuration for JTACAutoLase and start lasing for all JTACs smoke = "local smoke = false" - if self.settings.plugins[self.nameInSettings_useSmoke]: + if self.isUseSmoke(): smoke = "local smoke = true" lua = smoke + """ @@ -78,3 +85,11 @@ class JtacAutolasePlugin(BasePlugin): operation.injectLuaTrigger(lua, "Setting and starting JTACs") + def isUseSmoke(self) -> bool: + if not self.settings: + return False + + self.setSettings(self.settings) # create the necessary settings keys if needed + + return self.settings.plugins[self.nameInSettings_useSmoke] + diff --git a/plugin/liberation_plugin.py b/plugin/liberation_plugin.py index e9a01a39..ef97be3a 100644 --- a/plugin/liberation_plugin.py +++ b/plugin/liberation_plugin.py @@ -18,3 +18,6 @@ class LiberationPlugin(BasePlugin): def injectConfiguration(self, operation): if super().injectConfiguration(operation): pass + + def isEnabled(self) -> bool: + return True # mandatory plugin diff --git a/plugin/veaf_plugin.py b/plugin/veaf_plugin.py index 2e3b0531..c32cd86a 100644 --- a/plugin/veaf_plugin.py +++ b/plugin/veaf_plugin.py @@ -61,7 +61,7 @@ class VeafPlugin(BasePlugin): operation.injectPluginScript("veaf", "src\\scripts\\mist.lua", "mist") operation.injectPluginScript("veaf", "src\\scripts\\Moose.lua", "moose") operation.injectPluginScript("veaf", "src\\scripts\\CTLD.lua", "ctld") - operation.injectPluginScript("veaf", "src\\scripts\\NIOD.lua", "niod") + #operation.injectPluginScript("veaf", "src\\scripts\\NIOD.lua", "niod") operation.injectPluginScript("veaf", "src\\scripts\\WeatherMark.lua", "weathermark") operation.injectPluginScript("veaf", "src\\scripts\\veaf.lua", "veaf") operation.injectPluginScript("veaf", "src\\scripts\\dcsUnits.lua", "dcsunits") diff --git a/qt_ui/windows/settings/QSettingsWindow.py b/qt_ui/windows/settings/QSettingsWindow.py index 82010be9..88382ee0 100644 --- a/qt_ui/windows/settings/QSettingsWindow.py +++ b/qt_ui/windows/settings/QSettingsWindow.py @@ -183,28 +183,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() @@ -315,7 +297,8 @@ class QSettingsWindow(QDialog): self.pluginsGroup.setLayout(self.pluginsGroupLayout) row:int = 0 - for plugin in INSTALLED_PLUGINS: + for pluginName in INSTALLED_PLUGINS: + plugin = INSTALLED_PLUGINS[pluginName] plugin.setupUI(self, row) row = row + 1 @@ -342,8 +325,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) diff --git a/plugin/base/dcs_liberation.lua b/resources/plugins/base/dcs_liberation.lua similarity index 100% rename from plugin/base/dcs_liberation.lua rename to resources/plugins/base/dcs_liberation.lua diff --git a/plugin/base/json.lua b/resources/plugins/base/json.lua similarity index 100% rename from plugin/base/json.lua rename to resources/plugins/base/json.lua diff --git a/plugin/base/mist_4_3_74.lua b/resources/plugins/base/mist_4_3_74.lua similarity index 100% rename from plugin/base/mist_4_3_74.lua rename to resources/plugins/base/mist_4_3_74.lua diff --git a/plugin/custom/__plugins.lst.sample b/resources/plugins/custom/__plugins.lst.sample similarity index 100% rename from plugin/custom/__plugins.lst.sample rename to resources/plugins/custom/__plugins.lst.sample diff --git a/resources/plugins/doc/0.png b/resources/plugins/doc/0.png new file mode 100644 index 0000000000000000000000000000000000000000..3320b90882796fdb32556783d84bd7ead617f9fd GIT binary patch literal 25178 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 gzBTRrd0`--8`CB(>Z{p$M-BMJL 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 LRb@(~UIzSMOpY6~ literal 0 HcmV?d00001 diff --git a/plugin/jtacautolase/JTACAutoLase.lua b/resources/plugins/jtacautolase/JTACAutoLase.lua similarity index 100% rename from plugin/jtacautolase/JTACAutoLase.lua rename to resources/plugins/jtacautolase/JTACAutoLase.lua diff --git a/plugin/veaf b/resources/plugins/veaf similarity index 100% rename from plugin/veaf rename to resources/plugins/veaf From 5463505787e4105ffc65c0e251388358a481e361 Mon Sep 17 00:00:00 2001 From: David Pierron Date: Mon, 12 Oct 2020 20:01:27 +0200 Subject: [PATCH 06/10] added documentation for plugins system --- resources/plugins/doc/plugins_readme.md | 58 +++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 resources/plugins/doc/plugins_readme.md diff --git a/resources/plugins/doc/plugins_readme.md b/resources/plugins/doc/plugins_readme.md new file mode 100644 index 00000000..41c90955 --- /dev/null +++ b/resources/plugins/doc/plugins_readme.md @@ -0,0 +1,58 @@ +# 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. + +## 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 + +For more information, please visit the [VEAF Framework documentation site](https://veaf.github.io/VEAF-Mission-Creation-Tools/) (work in progress) + +## Custom plugins + +Custom scripts can also be injected by dropping them in the `resources/plugins/custom` folder, and writing a `__plugins.lst` file listing them in order. +See the `__plugins.lst.sample` file for an example. + +## New settings pages + +![New settings pages](0.png "New settings pages") + +Custom plugins can be enabled or disabled in the new *LUA Plugins* settings page. + +![LUA Plugins settings page](1.png "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. + +![LUA Plugins Options settings page](2.png "LUA Plugins settings page") + + From 373924a9590c180e7956c4a5a12e812ed3020baa Mon Sep 17 00:00:00 2001 From: David Pierron Date: Thu, 15 Oct 2020 16:21:39 +0200 Subject: [PATCH 07/10] small changes to veaf plugin --- plugin/veaf_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/veaf_plugin.py b/plugin/veaf_plugin.py index c32cd86a..963ca4f8 100644 --- a/plugin/veaf_plugin.py +++ b/plugin/veaf_plugin.py @@ -86,5 +86,5 @@ class VeafPlugin(BasePlugin): def injectConfiguration(self, operation): if super().injectConfiguration(operation): - operation.injectPluginScript("veaf", "src\\config\\missionConfig.lua", "missionconfig") + operation.injectPluginScript("veaf", "src\\config\\missionConfig.lua", "veaf-config") From ed92e9afb9ea7abc39a34c5883957ec778336681 Mon Sep 17 00:00:00 2001 From: David Pierron Date: Sun, 18 Oct 2020 18:23:31 +0200 Subject: [PATCH 08/10] changed the system to make use of JSON files --- .gitignore | 2 - game/game.py | 5 +- game/operation/operation.py | 8 +- game/settings.py | 5 +- gen/armor.py | 5 +- plugin/__init__.py | 12 +- plugin/base_plugin.py | 48 - plugin/jtacautolase_plugin.py | 95 - plugin/liberation_plugin.py | 23 - plugin/luaplugin.py | 199 + plugin/manager.py | 43 + plugin/veaf_plugin.py | 90 - qt_ui/windows/settings/QSettingsWindow.py | 100 +- resources/plugins/{doc => _doc}/0.png | Bin resources/plugins/{doc => _doc}/1.png | Bin resources/plugins/{doc => _doc}/2.png | Bin resources/plugins/_doc/plugins_readme.md | 84 + resources/plugins/base/plugin.json | 22 + resources/plugins/custom/__plugins.lst.sample | 29 - resources/plugins/doc/plugins_readme.md | 58 - .../jtacautolase/jtacautolase-config.lua | 38 + .../plugins/jtacautolase/mist_4_3_74.lua | 6822 +++++++++++++++++ resources/plugins/jtacautolase/plugin.json | 28 + resources/plugins/plugins.json | 5 + resources/plugins/veaf | 1 - 25 files changed, 7305 insertions(+), 417 deletions(-) delete mode 100644 plugin/base_plugin.py delete mode 100644 plugin/jtacautolase_plugin.py delete mode 100644 plugin/liberation_plugin.py create mode 100644 plugin/luaplugin.py create mode 100644 plugin/manager.py delete mode 100644 plugin/veaf_plugin.py rename resources/plugins/{doc => _doc}/0.png (100%) rename resources/plugins/{doc => _doc}/1.png (100%) rename resources/plugins/{doc => _doc}/2.png (100%) create mode 100644 resources/plugins/_doc/plugins_readme.md create mode 100644 resources/plugins/base/plugin.json delete mode 100644 resources/plugins/custom/__plugins.lst.sample delete mode 100644 resources/plugins/doc/plugins_readme.md create mode 100644 resources/plugins/jtacautolase/jtacautolase-config.lua create mode 100644 resources/plugins/jtacautolase/mist_4_3_74.lua create mode 100644 resources/plugins/jtacautolase/plugin.json create mode 100644 resources/plugins/plugins.json delete mode 160000 resources/plugins/veaf diff --git a/.gitignore b/.gitignore index ec279d3d..26831339 100644 --- a/.gitignore +++ b/.gitignore @@ -21,5 +21,3 @@ logs/ qt_ui/logs/liberation.log *.psd -resources/plugins/custom/__plugins.lst -resources/plugins/custom/*.lua diff --git a/game/game.py b/game/game.py index e38b140f..00ca8b5f 100644 --- a/game/game.py +++ b/game/game.py @@ -28,7 +28,7 @@ from .event.event import Event, UnitsDeliveryEvent from .event.frontlineattack import FrontlineAttackEvent from .infos.information import Information from .settings import Settings -from plugin import INSTALLED_PLUGINS +from plugin import LuaPluginManager COMMISION_UNIT_VARIETY = 4 COMMISION_LIMITS_SCALE = 1.5 @@ -220,8 +220,7 @@ class Game: ObjectiveDistanceCache.set_theater(self.theater) # set the settings in all plugins - for pluginName in INSTALLED_PLUGINS: - plugin = INSTALLED_PLUGINS[pluginName] + for plugin in LuaPluginManager().getPlugins(): plugin.setSettings(self.settings) def pass_turn(self, no_action=False): diff --git a/game/operation/operation.py b/game/operation/operation.py index e42d7b14..c64d0992 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -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 BasePlugin, INSTALLED_PLUGINS +from plugin import LuaPluginManager class Operation: attackers_starting_position = None # type: db.StartingPosition @@ -146,10 +146,7 @@ class Operation: if not scriptFileMnemonic in self.listOfPluginsScripts: self.listOfPluginsScripts.append(scriptFileMnemonic) - if pluginName == None: - pluginName = "custom" plugin_path = Path("./resources/plugins",pluginName) - logging.debug(f"plugin_path = {plugin_path}") if scriptFile != None: scriptFile_path = Path(plugin_path, scriptFile) @@ -469,8 +466,7 @@ dcsLiberation.TargetPoints = { # Inject Plugins Lua Scripts and data self.listOfPluginsScripts = [] - for pluginName in INSTALLED_PLUGINS: - plugin = INSTALLED_PLUGINS[pluginName] + for plugin in LuaPluginManager().getPlugins(): plugin.injectScripts(self) plugin.injectConfiguration(self) diff --git a/game/settings.py b/game/settings.py index bb7754d4..7b398330 100644 --- a/game/settings.py +++ b/game/settings.py @@ -1,4 +1,4 @@ -from plugin import INSTALLED_PLUGINS +from plugin import LuaPluginManager class Settings: @@ -41,8 +41,7 @@ class Settings: # LUA Plugins system self.plugins = {} - for pluginName in INSTALLED_PLUGINS: - plugin = INSTALLED_PLUGINS[pluginName] + for plugin in LuaPluginManager().getPlugins(): plugin.setSettings(self) diff --git a/gen/armor.py b/gen/armor.py index 21ae5a25..2873002a 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -34,7 +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 INSTALLED_PLUGINS +from plugin import LuaPluginManager SPREAD_DISTANCE_FACTOR = 0.1, 0.3 SPREAD_DISTANCE_SIZE_FACTOR = 0.1 @@ -140,7 +140,8 @@ 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 - useJTAC = INSTALLED_PLUGINS and INSTALLED_PLUGINS["JtacAutolasePlugin"] and INSTALLED_PLUGINS["JtacAutolasePlugin"].isEnabled() + 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) diff --git a/plugin/__init__.py b/plugin/__init__.py index 2bbf459c..37f3c6d4 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -1,10 +1,2 @@ -from .base_plugin import BasePlugin -from .veaf_plugin import VeafPlugin -from .jtacautolase_plugin import JtacAutolasePlugin -from .liberation_plugin import LiberationPlugin - -INSTALLED_PLUGINS={ - "VeafPlugin": VeafPlugin(), - "JtacAutolasePlugin": JtacAutolasePlugin(), - "LiberationPlugin": LiberationPlugin(), -} +from .luaplugin import LuaPlugin +from .manager import LuaPluginManager \ No newline at end of file diff --git a/plugin/base_plugin.py b/plugin/base_plugin.py deleted file mode 100644 index 4eb6e744..00000000 --- a/plugin/base_plugin.py +++ /dev/null @@ -1,48 +0,0 @@ -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 - -class BasePlugin(): - nameInUI:str = "Base plugin" - nameInSettings:str = "plugin.base" - enabledDefaultValue:bool = False - - def __init__(self): - self.uiWidget: QCheckBox = None - self.enabled = self.enabledDefaultValue - self.settings = None - - def setupUI(self, settingsWindow, row:int): - self.settings = settingsWindow.game.settings - - self.uiWidget = 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) - - def setSettings(self, settings): - self.settings = settings - if not self.nameInSettings in self.settings.plugins: - self.settings.plugins[self.nameInSettings] = self.enabledDefaultValue - - def applySetting(self, settingsWindow): - self.settings.plugins[self.nameInSettings] = self.uiWidget.isChecked() - self.enabled = self.settings.plugins[self.nameInSettings] - - def injectScripts(self, operation): - self.settings = operation.game.settings - return self.isEnabled() - - def injectConfiguration(self, operation): - self.settings = operation.game.settings - 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] diff --git a/plugin/jtacautolase_plugin.py b/plugin/jtacautolase_plugin.py deleted file mode 100644 index 8b0c316c..00000000 --- a/plugin/jtacautolase_plugin.py +++ /dev/null @@ -1,95 +0,0 @@ -from dcs.triggers import TriggerStart -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 -from .base_plugin import BasePlugin - -class JtacAutolasePlugin(BasePlugin): - nameInUI:str = "JTAC Autolase" - nameInSettings:str = "plugin.jtacAutolase" - enabledDefaultValue:bool = True - - #Allow spawn option - nameInUI_useSmoke:str = "JTACs use smoke" - nameInSettings_useSmoke:str = "plugin.jtacAutolase.useSmoke" - enabledDefaultValue_useSmoke:bool = True - - def setupUI(self, settingsWindow, row:int): - # call the base method to add the plugin selection checkbox - super().setupUI(settingsWindow, row) - - if settingsWindow.pluginsOptionsPageLayout: - self.optionsGroup = QGroupBox(self.nameInUI) - optionsGroupLayout = QGridLayout(); - optionsGroupLayout.setAlignment(Qt.AlignTop) - self.optionsGroup.setLayout(optionsGroupLayout) - settingsWindow.pluginsOptionsPageLayout.addWidget(self.optionsGroup) - - # JTAC use smoke - if not self.nameInSettings_useSmoke in self.settings.plugins: - self.settings.plugins[self.nameInSettings_useSmoke] = True - - self.uiWidget_useSmoke = QCheckBox() - self.uiWidget_useSmoke.setChecked(self.settings.plugins[self.nameInSettings_useSmoke]) - self.uiWidget_useSmoke.toggled.connect(lambda: self.applySetting(settingsWindow)) - - optionsGroupLayout.addWidget(QLabel(self.nameInUI_useSmoke), 0, 0) - optionsGroupLayout.addWidget(self.uiWidget_useSmoke, 0, 1, Qt.AlignRight) - - # disable or enable the UI in the plugins special page - self.enableOptionsGroup() - - def enableOptionsGroup(self): - pluginEnabled = self.uiWidget.isChecked() - self.optionsGroup.setEnabled(pluginEnabled) - - def setSettings(self, settings): - # call the base method - super().setSettings(settings) - if not self.nameInSettings_useSmoke in self.settings.plugins: - self.settings.plugins[self.nameInSettings_useSmoke] = self.enabledDefaultValue_useSmoke - - def applySetting(self, settingsWindow): - # call the base method to apply the plugin selection checkbox value - super().applySetting(settingsWindow) - - # save the "use smoke" option - self.settings.plugins[self.nameInSettings_useSmoke] = self.uiWidget_useSmoke.isChecked() - - # disable or enable the UI in the plugins special page - self.enableOptionsGroup() - - def injectScripts(self, operation): - if super().injectScripts(operation): - operation.injectPluginScript("jtacautolase", "JTACAutoLase.lua", "jtacautolase") - - def injectConfiguration(self, operation): - if super().injectConfiguration(operation): - - # add a configuration for JTACAutoLase and start lasing for all JTACs - smoke = "local smoke = false" - if self.isUseSmoke(): - smoke = "local smoke = true" - - lua = smoke + """ - - -- setting and starting JTACs - env.info("DCSLiberation|: setting and starting JTACs") - - for _, jtac in pairs(dcsLiberation.JTACs) do - if dcsLiberation.JTACAutoLase then - dcsLiberation.JTACAutoLase(jtac.dcsUnit, jtac.code, smoke, 'vehicle') - end - end - """ - - operation.injectLuaTrigger(lua, "Setting and starting JTACs") - - def isUseSmoke(self) -> bool: - if not self.settings: - return False - - self.setSettings(self.settings) # create the necessary settings keys if needed - - return self.settings.plugins[self.nameInSettings_useSmoke] - diff --git a/plugin/liberation_plugin.py b/plugin/liberation_plugin.py deleted file mode 100644 index ef97be3a..00000000 --- a/plugin/liberation_plugin.py +++ /dev/null @@ -1,23 +0,0 @@ -from .base_plugin import BasePlugin - -class LiberationPlugin(BasePlugin): - nameInUI:str = "Liberation script" - nameInSettings:str = "plugin.liberation" - enabledDefaultValue:bool = True - - def setupUI(self, settingsWindow, row:int): - # Don't setup any UI, this plugin is mandatory - pass - - def injectScripts(self, operation): - if super().injectScripts(operation): - operation.injectPluginScript("base", "mist_4_3_74.lua", "mist") - operation.injectPluginScript("base", "json.lua", "json") - operation.injectPluginScript("base", "dcs_liberation.lua", "liberation") - - def injectConfiguration(self, operation): - if super().injectConfiguration(operation): - pass - - def isEnabled(self) -> bool: - return True # mandatory plugin diff --git a/plugin/luaplugin.py b/plugin/luaplugin.py new file mode 100644 index 00000000..8f92ad76 --- /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.settings = 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.settings = 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.settings = 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/plugin/veaf_plugin.py b/plugin/veaf_plugin.py deleted file mode 100644 index 963ca4f8..00000000 --- a/plugin/veaf_plugin.py +++ /dev/null @@ -1,90 +0,0 @@ -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 -from .base_plugin import BasePlugin - -class VeafPlugin(BasePlugin): - nameInUI:str = "VEAF framework" - nameInSettings:str = "plugin.veaf" - enabledDefaultValue:bool = False - - #Allow spawn option - nameInUI_allowSpawn:str = "Allow units spawn via markers and CTLD (not implemented yet)" - nameInSettings_allowSpawn:str = "plugin.veaf.allowSpawn" - - def setupUI(self, settingsWindow, row:int): - # call the base method to add the plugin selection checkbox - super().setupUI(settingsWindow, row) - - if settingsWindow.pluginsOptionsPageLayout: - self.optionsGroup = QGroupBox(self.nameInUI) - optionsGroupLayout = QGridLayout(); - optionsGroupLayout.setAlignment(Qt.AlignTop) - self.optionsGroup.setLayout(optionsGroupLayout) - settingsWindow.pluginsOptionsPageLayout.addWidget(self.optionsGroup) - - # allow spawn of objects - if not self.nameInSettings_allowSpawn in self.settings.plugins: - self.settings.plugins[self.nameInSettings_allowSpawn] = True - - self.uiWidget_allowSpawn = QCheckBox() - self.uiWidget_allowSpawn.setChecked(self.settings.plugins[self.nameInSettings_allowSpawn]) - self.uiWidget_allowSpawn.setEnabled(False) - self.uiWidget_allowSpawn.toggled.connect(lambda: self.applySetting(settingsWindow)) - - optionsGroupLayout.addWidget(QLabel(self.nameInUI_allowSpawn), 0, 0) - optionsGroupLayout.addWidget(self.uiWidget_allowSpawn, 0, 1, Qt.AlignRight) - - # disable or enable the UI in the plugins special page - self.enableOptionsGroup() - - def enableOptionsGroup(self): - pluginEnabled = self.uiWidget.isChecked() - self.optionsGroup.setEnabled(pluginEnabled) - - def applySetting(self, settingsWindow): - # call the base method to apply the plugin selection checkbox value - super().applySetting(settingsWindow) - - # save the "allow spawn" option - self.settings.plugins[self.nameInSettings_allowSpawn] = self.uiWidget_allowSpawn.isChecked() - - # disable or enable the UI in the plugins special page - self.enableOptionsGroup() - - def injectScripts(self, operation): - if super().injectScripts(operation): - # bypass JTACAutoLase - operation.bypassPluginScript("veaf", "jtacautolase") - - # inject the required scripts - operation.injectPluginScript("veaf", "src\\scripts\\mist.lua", "mist") - operation.injectPluginScript("veaf", "src\\scripts\\Moose.lua", "moose") - operation.injectPluginScript("veaf", "src\\scripts\\CTLD.lua", "ctld") - #operation.injectPluginScript("veaf", "src\\scripts\\NIOD.lua", "niod") - operation.injectPluginScript("veaf", "src\\scripts\\WeatherMark.lua", "weathermark") - operation.injectPluginScript("veaf", "src\\scripts\\veaf.lua", "veaf") - operation.injectPluginScript("veaf", "src\\scripts\\dcsUnits.lua", "dcsunits") - operation.injectPluginScript("veaf", "src\\scripts\\veafAssets.lua", "veafassets") - operation.injectPluginScript("veaf", "src\\scripts\\veafCarrierOperations.lua", "veafcarrieroperations") - operation.injectPluginScript("veaf", "src\\scripts\\veafCasMission.lua", "veafcasmission") - operation.injectPluginScript("veaf", "src\\scripts\\veafCombatMission.lua", "veafcombatmission") - operation.injectPluginScript("veaf", "src\\scripts\\veafCombatZone.lua", "veafcombatzone") - operation.injectPluginScript("veaf", "src\\scripts\\veafGrass.lua", "veafgrass") - operation.injectPluginScript("veaf", "src\\scripts\\veafInterpreter.lua", "veafinterpreter") - operation.injectPluginScript("veaf", "src\\scripts\\veafMarkers.lua", "veafmarkers") - operation.injectPluginScript("veaf", "src\\scripts\\veafMove.lua", "veafmove") - operation.injectPluginScript("veaf", "src\\scripts\\veafNamedPoints.lua", "veafnamedpoints") - operation.injectPluginScript("veaf", "src\\scripts\\veafRadio.lua", "veafradio") - operation.injectPluginScript("veaf", "src\\scripts\\veafRemote.lua", "veafremote") - operation.injectPluginScript("veaf", "src\\scripts\\veafSecurity.lua", "veafsecurity") - operation.injectPluginScript("veaf", "src\\scripts\\veafShortcuts.lua", "veafshortcuts") - operation.injectPluginScript("veaf", "src\\scripts\\veafSpawn.lua", "veafspawn") - operation.injectPluginScript("veaf", "src\\scripts\\veafTransportMission.lua", "veaftransportmission") - operation.injectPluginScript("veaf", "src\\scripts\\veafUnits.lua", "veafunits") - - - def injectConfiguration(self, operation): - if super().injectConfiguration(operation): - operation.injectPluginScript("veaf", "src\\config\\missionConfig.lua", "veaf-config") - diff --git a/qt_ui/windows/settings/QSettingsWindow.py b/qt_ui/windows/settings/QSettingsWindow.py index 88382ee0..c61372a6 100644 --- a/qt_ui/windows/settings/QSettingsWindow.py +++ b/qt_ui/windows/settings/QSettingsWindow.py @@ -11,7 +11,7 @@ from game.game import Game from game.infos.information import Information from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.finances.QFinancesMenu import QHorizontalSeparationLine -from plugin import BasePlugin, INSTALLED_PLUGINS +from plugin import LuaPluginManager class QSettingsWindow(QDialog): @@ -19,6 +19,8 @@ class QSettingsWindow(QDialog): super(QSettingsWindow, self).__init__() self.game = game + self.pluginsPage = None + self.pluginsOptionsPage = None self.setModal(True) self.setWindowTitle("Settings") @@ -37,53 +39,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) - - plugins = QStandardItem("LUA Plugins") - plugins.setIcon(CONST.ICONS["Plugins"]) - plugins.setEditable(False) - plugins.setSelectable(True) - - pluginsOptions = QStandardItem("LUA Plugins Options") - pluginsOptions.setIcon(CONST.ICONS["PluginsOptions"]) - pluginsOptions.setEditable(False) - pluginsOptions.setSelectable(True) - - self.categoryList.setIconSize(QSize(32, 32)) - self.categoryModel.appendRow(difficulty) - self.categoryModel.appendRow(generator) self.categoryModel.appendRow(cheat) - self.categoryModel.appendRow(plugins) - self.categoryModel.appendRow(pluginsOptions) + 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.initPluginsLayout() - - self.right_layout.addWidget(self.difficultyPage) - self.right_layout.addWidget(self.generatorPage) - self.right_layout.addWidget(self.cheatPage) - self.right_layout.addWidget(self.pluginsPage) - self.right_layout.addWidget(self.pluginsOptionsPage) self.layout.addWidget(self.categoryList, 0, 0, 1, 1) self.layout.addLayout(self.right_layout, 0, 1, 5, 1) @@ -281,28 +283,32 @@ class QSettingsWindow(QDialog): self.cheatLayout.addWidget(self.moneyCheatBox, 0, 0) def initPluginsLayout(self): - 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) - + uiPrepared = False row:int = 0 - for pluginName in INSTALLED_PLUGINS: - plugin = INSTALLED_PLUGINS[pluginName] - plugin.setupUI(self, row) - row = row + 1 + for plugin in LuaPluginManager().getPlugins(): + if plugin.hasUI(): + if not uiPrepared: + uiPrepared = True - self.pluginsPageLayout.addWidget(self.pluginsGroup) + 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) diff --git a/resources/plugins/doc/0.png b/resources/plugins/_doc/0.png similarity index 100% rename from resources/plugins/doc/0.png rename to resources/plugins/_doc/0.png diff --git a/resources/plugins/doc/1.png b/resources/plugins/_doc/1.png similarity index 100% rename from resources/plugins/doc/1.png rename to resources/plugins/_doc/1.png diff --git a/resources/plugins/doc/2.png b/resources/plugins/_doc/2.png similarity index 100% rename from resources/plugins/doc/2.png rename to resources/plugins/_doc/2.png 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 + +![New settings pages](0.png "New settings pages") + +Custom plugins can be enabled or disabled in the new *LUA Plugins* settings page. + +![LUA Plugins settings page](1.png "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. + +![LUA Plugins Options settings page](2.png "LUA Plugins settings page") + + 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/plugins/custom/__plugins.lst.sample b/resources/plugins/custom/__plugins.lst.sample deleted file mode 100644 index 27cbf0c0..00000000 --- a/resources/plugins/custom/__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/plugins/doc/plugins_readme.md b/resources/plugins/doc/plugins_readme.md deleted file mode 100644 index 41c90955..00000000 --- a/resources/plugins/doc/plugins_readme.md +++ /dev/null @@ -1,58 +0,0 @@ -# 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. - -## 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 - -For more information, please visit the [VEAF Framework documentation site](https://veaf.github.io/VEAF-Mission-Creation-Tools/) (work in progress) - -## Custom plugins - -Custom scripts can also be injected by dropping them in the `resources/plugins/custom` folder, and writing a `__plugins.lst` file listing them in order. -See the `__plugins.lst.sample` file for an example. - -## New settings pages - -![New settings pages](0.png "New settings pages") - -Custom plugins can be enabled or disabled in the new *LUA Plugins* settings page. - -![LUA Plugins settings page](1.png "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. - -![LUA Plugins Options settings page](2.png "LUA Plugins settings page") - - diff --git a/resources/plugins/jtacautolase/jtacautolase-config.lua b/resources/plugins/jtacautolase/jtacautolase-config.lua new file mode 100644 index 00000000..04d0c293 --- /dev/null +++ b/resources/plugins/jtacautolase/jtacautolase-config.lua @@ -0,0 +1,38 @@ +------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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 + veaf.logTrace("dcsLiberation") + + -- specific options + local smoke = false + + -- retrieve specific options values + if dcsLiberation.plugins then + veaf.logTrace("dcsLiberation.plugins") + + if dcsLiberation.plugins.jtacautolase then + veaf.logTrace("dcsLiberation.plugins.jtacautolase") + veaf.logTrace(string.format("dcsLiberation.plugins.jtacautolase.smoke=%s",veaf.p(dcsLiberation.plugins.jtacautolase.smoke))) + + smoke = dcsLiberation.plugins.jtacautolase.smoke + end + end + + veaf.logTrace(string.format("smoke=%s",veaf.p(smoke))) + + -- actual configuration code + for _, jtac in pairs(dcsLiberation.JTACs) do + if dcsLiberation.JTACAutoLase then + dcsLiberation.JTACAutoLase(jtac.dcsUnit, jtac.code, 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..837311f7 --- /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": "configuration.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/plugins/veaf b/resources/plugins/veaf deleted file mode 160000 index 219cdffe..00000000 --- a/resources/plugins/veaf +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 219cdffef087660fe448a41e1f187c4856e9d80f From 41d50204670328ce22bf573f7a38b0198d5a1fc2 Mon Sep 17 00:00:00 2001 From: David Pierron Date: Tue, 20 Oct 2020 22:25:39 +0200 Subject: [PATCH 09/10] bug correction - when running a new campaing without accessing the plugin setup screen --- plugin/luaplugin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugin/luaplugin.py b/plugin/luaplugin.py index 8f92ad76..7bc4f57a 100644 --- a/plugin/luaplugin.py +++ b/plugin/luaplugin.py @@ -73,7 +73,7 @@ class LuaPlugin(): def setupUI(self, settingsWindow, row:int): # set the game settings - self.settings = settingsWindow.game.settings + self.setSettings(settingsWindow.game.settings) if not self.skipUI: # create the plugin choice checkbox interface @@ -95,7 +95,7 @@ class LuaPlugin(): # browse each option in the specific options list row = 0 for specificOption in self.specificOptions: - nameInSettings = self.nameInSettings + specificOption.mnemonic + nameInSettings = self.nameInSettings + "." + specificOption.mnemonic if not nameInSettings in self.settings.plugins: self.settings.plugins[nameInSettings] = specificOption.defaultValue @@ -136,7 +136,7 @@ class LuaPlugin(): # do the same for each option in the specific options list for specificOption in self.specificOptions: - nameInSettings = self.nameInSettings + specificOption.mnemonic + nameInSettings = self.nameInSettings + "." + specificOption.mnemonic self.settings.plugins[nameInSettings] = specificOption.uiWidget.isChecked() # disable or enable the UI in the plugins special page @@ -144,7 +144,7 @@ class LuaPlugin(): def injectScripts(self, operation): # set the game settings - self.settings = operation.game.settings + self.setSettings(operation.game.settings) # execute the work order if self.scriptsWorkOrders != None: @@ -156,13 +156,13 @@ class LuaPlugin(): def injectConfiguration(self, operation): # set the game settings - self.settings = operation.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 + nameInSettings = self.nameInSettings + "." + specificOption.mnemonic value = "true" if self.settings.plugins[nameInSettings] else "false" defineAllOptions += f" dcsLiberation.plugins.{self.mnemonic}.{specificOption.mnemonic} = {value} \n" From 44c976948dc0913303a8f74b74b2c9aa18b60e22 Mon Sep 17 00:00:00 2001 From: David Pierron Date: Tue, 20 Oct 2020 23:02:46 +0200 Subject: [PATCH 10/10] bug correction in the JTACautolase LUA config --- game/operation/operation.py | 6 ------ .../jtacautolase/jtacautolase-config.lua | 17 ++++++++--------- resources/plugins/jtacautolase/plugin.json | 2 +- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/game/operation/operation.py b/game/operation/operation.py index 3284c800..c83f903f 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -369,12 +369,6 @@ 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 """ # Process the tankers lua += """ diff --git a/resources/plugins/jtacautolase/jtacautolase-config.lua b/resources/plugins/jtacautolase/jtacautolase-config.lua index 04d0c293..47a88edf 100644 --- a/resources/plugins/jtacautolase/jtacautolase-config.lua +++ b/resources/plugins/jtacautolase/jtacautolase-config.lua @@ -9,29 +9,28 @@ env.info("DCSLiberation|JTACAutolase plugin - configuration") if dcsLiberation then - veaf.logTrace("dcsLiberation") + env.info(string.format("DCSLiberation|JTACAutolase plugin - dcsLiberation")) -- specific options local smoke = false -- retrieve specific options values if dcsLiberation.plugins then - veaf.logTrace("dcsLiberation.plugins") + env.info(string.format("DCSLiberation|JTACAutolase plugin - dcsLiberation.plugins")) if dcsLiberation.plugins.jtacautolase then - veaf.logTrace("dcsLiberation.plugins.jtacautolase") - veaf.logTrace(string.format("dcsLiberation.plugins.jtacautolase.smoke=%s",veaf.p(dcsLiberation.plugins.jtacautolase.smoke))) - + 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 - veaf.logTrace(string.format("smoke=%s",veaf.p(smoke))) - -- actual configuration code for _, jtac in pairs(dcsLiberation.JTACs) do - if dcsLiberation.JTACAutoLase then - dcsLiberation.JTACAutoLase(jtac.dcsUnit, jtac.code, smoke, 'vehicle') + 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 diff --git a/resources/plugins/jtacautolase/plugin.json b/resources/plugins/jtacautolase/plugin.json index 837311f7..33ce09dd 100644 --- a/resources/plugins/jtacautolase/plugin.json +++ b/resources/plugins/jtacautolase/plugin.json @@ -21,7 +21,7 @@ ], "configurationWorkOrders": [ { - "file": "configuration.lua", + "file": "jtacautolase-config.lua", "mnemonic": "jtacautolase-config" } ]