------------------------------------------------------------------ -- VEAF carrier command and functions for DCS World -- By zip (2018) -- -- Features: -- --------- -- * Radio menus allow starting and ending carrier operations. Carriers go back to their initial point when operations are ended -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafCarrierOperations = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafCarrierOperations.Id = "CARRIER" --- Version. veafCarrierOperations.Version = "1.12.2" -- trace level, specific to this module --veafCarrierOperations.LogLevel = "trace" veaf.loggers.new(veafCarrierOperations.Id, veafCarrierOperations.LogLevel) veafCarrierOperations.RadioMenuName = "CARRIER OPS" veafCarrierOperations.RadioMenuNameBlue = "CARRIER OPS - BLUE" veafCarrierOperations.RadioMenuNameRed = "CARRIER OPS - RED" veafCarrierOperations.DisableSecurity = false veafCarrierOperations.AllCarriers = { ["LHA_Tarawa"] = { runwayAngleWithBRC = -1, desiredWindSpeedOnDeck = 20}, ["Stennis"] = { runwayAngleWithBRC = 9.05, desiredWindSpeedOnDeck = 25}, ["CVN_71"] = { runwayAngleWithBRC = 9.05, desiredWindSpeedOnDeck = 25}, ["CVN_72"] = { runwayAngleWithBRC = 9.05, desiredWindSpeedOnDeck = 25}, ["CVN_73"] = { runwayAngleWithBRC = 9.05, desiredWindSpeedOnDeck = 25}, ["CVN_75"] = { runwayAngleWithBRC = 9.05, desiredWindSpeedOnDeck = 25}, ["Forrestal"] = { runwayAngleWithBRC = 9.05, desiredWindSpeedOnDeck = 25}, ["KUZNECOW"] ={ runwayAngleWithBRC = 9, desiredWindSpeedOnDeck = 25}, ["CV_1143_5"] ={ runwayAngleWithBRC = 9, desiredWindSpeedOnDeck = 25} } veafCarrierOperations.ALT_FOR_MEASURING_WIND = 30 -- wind is measured at 30 meters, 10 meters above deck veafCarrierOperations.ALIGNMENT_MANOEUVER_SPEED = 20 * 0.51445 -- carrier speed when not yet aligned to the wind (in m/s) veafCarrierOperations.MAX_OPERATIONS_DURATION = 45 -- operations are stopped after (minutes) veafCarrierOperations.SCHEDULER_INTERVAL = 1 -- scheduler runs every minute veafCarrierOperations.MIN_WINDSPEED_FOR_CHANGING_HEADING = 4 * 0.51445 -- don't deroute the carrier if the wind speed is lower than this (m/s) veafCarrierOperations.MIN_CARRIER_SPEED = 4 * 0.51445 -- don't make the carrier steam at less than this speed (m/s) veafCarrierOperations.RemoteCommandParser = "([[a-zA-Z0-9]+)%s?([^%s]*)%s?(.*)" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Radio menus paths veafCarrierOperations.rootPath = nil veafCarrierOperations.rootPathBlue = nil veafCarrierOperations.rootPathRed = nil --- Carrier groups data, for Carrier Operations commands veafCarrierOperations.carriers = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafCarrierOperations.debugMarkersErasedAtEachStep = {} veafCarrierOperations.traceMarkerId = 2727 function veafCarrierOperations.getDebugMarkersErasedAtEachStep(name) if not name then return nil end if not veafCarrierOperations.debugMarkersErasedAtEachStep then veafCarrierOperations.debugMarkersErasedAtEachStep = {} end if not veafCarrierOperations.debugMarkersErasedAtEachStep[name] then veafCarrierOperations.debugMarkersErasedAtEachStep[name] = {} end return veafCarrierOperations.debugMarkersErasedAtEachStep[name] end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Carrier operations commands ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Start carrier operations ; changes the radio menu item to END and make the carrier move function veafCarrierOperations.startCarrierOperations(parameters) veaf.loggers.get(veafCarrierOperations.Id):debug("startCarrierOperations()") veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("Parameters for this command are : %s",veaf.p(parameters))) local carrierInfo, userUnitName = veaf.safeUnpack(parameters) local groupName, duration = veaf.safeUnpack(carrierInfo) veaf.loggers.get(veafCarrierOperations.Id):debug(string.format("Carrier groupName : %s",veaf.p(groupName))) veaf.loggers.get(veafCarrierOperations.Id):debug(string.format("duration : %s",veaf.p(duration))) veaf.loggers.get(veafCarrierOperations.Id):debug(string.format("userUnitName : %s",veaf.p(userUnitName))) local carrier = veafCarrierOperations.carriers[groupName] if not(carrier) then local text = "Cannot find the carrier group "..groupName veaf.loggers.get(veafCarrierOperations.Id):error(text) veaf.outTextForGroup(userUnitName, text, 5) return end -- find the actual carrier unit local group = Group.getByName(groupName) for _, unit in pairs(group:getUnits()) do local unitType = unit:getDesc()["typeName"] for knownCarrierType, data in pairs(veafCarrierOperations.AllCarriers) do if unitType == knownCarrierType then carrier.carrierUnitName = unit:getName() carrier.pedroUnitName = carrier.carrierUnitName .. " Pedro" -- rescue helo unit name carrier.tankerUnitName = carrier.carrierUnitName .. " S3B-Tanker" -- emergency tanker unit name carrier.tankerRouteSet = 0 carrier.runwayAngleWithBRC = data.runwayAngleWithBRC carrier.desiredWindSpeedOnDeck = data.desiredWindSpeedOnDeck carrier.initialPosition = unit:getPosition().p veaf.loggers.get(veafCarrierOperations.Id):trace("initialPosition="..veaf.vecToString(carrier.initialPosition)) break end end end carrier.conductingAirOperations = true carrier.airOperationsStartedAt = timer.getTime() carrier.airOperationsEndAt = carrier.airOperationsStartedAt + duration * 60 veafCarrierOperations.continueCarrierOperations(groupName) -- will update the *carrier* structure local text = veafCarrierOperations.getAtcForCarrierOperations(groupName) .. "\n\nGetting a good alignment may require up to 5 minutes" veaf.loggers.get(veafCarrierOperations.Id):info(text) veaf.outTextForGroup(userUnitName, text, 25) -- change the menu veaf.loggers.get(veafCarrierOperations.Id):trace("change the menu") veafCarrierOperations.rebuildRadioMenu() end --- Continue carrier operations ; make the carrier move according to the wind. Called by startCarrierOperations and by the scheduler. function veafCarrierOperations.continueCarrierOperations(groupName, userUnitName) veaf.loggers.get(veafCarrierOperations.Id):debug("continueCarrierOperations(".. groupName .. ")") local carrier = veafCarrierOperations.carriers[groupName] if not(carrier) then local text = "Cannot find the carrier group "..groupName veaf.loggers.get(veafCarrierOperations.Id):error(text) veaf.outTextForGroup(userUnitName, text, 5) return end -- find the actual carrier unit local carrierUnit = Unit.getByName(carrier.carrierUnitName) -- take note of the starting position local startPosition = veaf.getAvgGroupPos(groupName) local currentHeading = 0 if carrierUnit then startPosition = carrierUnit:getPosition().p veaf.loggers.get(veafCarrierOperations.Id):trace("startPosition (raw) ="..veaf.vecToString(startPosition)) currentHeading = mist.utils.round(mist.utils.toDegree(mist.getHeading(carrierUnit, true)), 0) end veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("currentHeading=%s", veaf.p(currentHeading))) startPosition = { x=startPosition.x, z=startPosition.z, y=startPosition.y + veafCarrierOperations.ALT_FOR_MEASURING_WIND} -- on deck, 50 meters above the water veaf.loggers.get(veafCarrierOperations.Id):trace("startPosition="..veaf.vecToString(startPosition)) veaf.loggers.get(veafCarrierOperations.Id):cleanupMarkers(veafCarrierOperations.getDebugMarkersErasedAtEachStep(carrier.carrierUnitName)) --veafCarrierOperations.traceMarkerId = veaf.loggers.get(veafCarrierOperations.Id):marker(veafCarrierOperations.traceMarkerId, "CARRIER", "startPosition", startPosition, veafCarrierOperations.getDebugMarkersErasedAtEachStep(carrier.carrierUnitName)) local carrierDistanceFromInitialPosition = ((startPosition.x - carrier.initialPosition.x)^2 + (startPosition.z - carrier.initialPosition.z)^2)^0.5 veaf.loggers.get(veafCarrierOperations.Id):trace("carrierDistanceFromInitialPosition="..carrierDistanceFromInitialPosition) -- compute magnetic deviation at carrier position -- let's not use mist.getNorthCorrection, it's not computing magnetic deviation... -- TODO find how to actually compute it --[[ local magdev = veaf.round(mist.getNorthCorrection(startPosition) * 180 / math.pi,1) veaf.loggers.get(veafCarrierOperations.Id):trace("magdev = " .. magdev) ]] -- make the carrier move if startPosition ~= nil then local dir = currentHeading -- start with current heading --get wind info local wind = atmosphere.getWind(startPosition) veaf.loggers.get(veafCarrierOperations.Id):trace("wind=%s", veaf.p(wind)) local windspeed = mist.vec.mag(wind) veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("windspeed=%s", veaf.p(windspeed))) if windspeed >= veafCarrierOperations.MIN_WINDSPEED_FOR_CHANGING_HEADING then --get wind direction sorted if wind.x ~= 0 then dir = veaf.round(math.atan2(wind.z, wind.x) * 180 / math.pi,0) elseif wind.z < 0 then dir = 270 elseif wind.z > 0 then dir = 90 elseif wind.z == 0 then dir = carrier.heading end if dir < 0 then dir = dir + 360 --converts to positive numbers end if dir <= 180 then dir = dir + 180 else dir = dir - 180 end veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("wind direction=%s", veaf.p(dir))) dir = veaf.round(dir + carrier.runwayAngleWithBRC) --to account for angle of landing deck and movement of the ship end if dir > 360 then dir = dir - 360 end veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("dir=%s", veaf.p(dir))) local speed = 1 local desiredWindSpeedOnDeck = carrier.desiredWindSpeedOnDeck * 0.51445 if desiredWindSpeedOnDeck < 1 then desiredWindSpeedOnDeck = 1 end -- minimum 1 m/s if windspeed < desiredWindSpeedOnDeck then speed = desiredWindSpeedOnDeck - windspeed end if speed < veafCarrierOperations.MIN_CARRIER_SPEED then speed = veafCarrierOperations.MIN_CARRIER_SPEED end veaf.loggers.get(veafCarrierOperations.Id):trace("BRC speed="..speed.." m/s") -- compute a new waypoint local headingRad = mist.utils.toRadian(dir) local length = 4000 local newWaypoint = { x = startPosition.x + length * math.cos(headingRad), z = startPosition.z + length * math.sin(headingRad), y = startPosition.y } -- check for obstructions local carrierGroup = Group.getByName(groupName) local unitsToCheck = {} if carrierGroup then for _, unitToCheck in pairs(carrierGroup:getUnits()) do veaf.loggers.get(veafCarrierOperations.Id):trace("checking %s %s", veaf.p(unitToCheck:getTypeName()), veaf.p(unitToCheck:getName())) if not carrierUnit or unitToCheck:getID() ~= carrierUnit:getID() then table.insert(unitsToCheck, unitToCheck) end end end veaf.loggers.get(veafCarrierOperations.Id):trace("unitsToCheck=%s", veaf.p(unitsToCheck)) local pointA = veaf.computeCoordinatesOffsetFromRoute(startPosition, newWaypoint, 500, 500) local pointB = veaf.computeCoordinatesOffsetFromRoute(startPosition, newWaypoint, 500, -500) local pointC = veaf.computeCoordinatesOffsetFromRoute(startPosition, newWaypoint, 2000, -500) local pointD = veaf.computeCoordinatesOffsetFromRoute(startPosition, newWaypoint, 2000, 500) local polygon = {pointA, pointB, pointC, pointD} veaf.loggers.get(veafCarrierOperations.Id):trace("polygon=%s", veaf.p(polygon)) veafCarrierOperations.traceMarkerId = veaf.loggers.get(veafCarrierOperations.Id):markerQuad(veafCarrierOperations.traceMarkerId, "CARRIER", "obstructionsCheck", {pointA, pointB, pointC, pointD}, veafCarrierOperations.getDebugMarkersErasedAtEachStep(carrier.carrierUnitName), VeafDrawingOnMap.LINE_TYPE["dashed"], {1, 0, 0, 0.5}) local obstructions = {} for i =1, #unitsToCheck do local lUnit = unitsToCheck[i] veaf.loggers.get(veafCarrierOperations.Id):trace("lUnit:getName()=%s", veaf.p(lUnit:getName())) if mist.pointInPolygon(lUnit:getPosition().p, polygon) then obstructions[#obstructions + 1] = lUnit end end veaf.loggers.get(veafCarrierOperations.Id):trace("obstructions=%s", veaf.p(obstructions)) if #obstructions > 0 then -- obstructions found, derouting local newDir = dir + 90 if newDir > 360 then newDir = newDir - 360 end local msg = string.format("Obstruction found at heading %s, derouting %s to heading %s", veaf.p(#obstructions), veaf.p(dir), veaf.p(groupName), veaf.p(newDir)) veaf.loggers.get(veafCarrierOperations.Id):debug(msg) veaf.outTextForGroup(userUnitName, msg, 5) headingRad = mist.utils.toRadian(newDir) length = 4000 newWaypoint = { x = startPosition.x + length * math.cos(headingRad), z = startPosition.z + length * math.sin(headingRad), y = startPosition.y } end veaf.loggers.get(veafCarrierOperations.Id):trace("headingRad="..headingRad) veaf.loggers.get(veafCarrierOperations.Id):trace("length="..length) veaf.loggers.get(veafCarrierOperations.Id):trace("newWaypoint="..veaf.vecToString(newWaypoint)) veafCarrierOperations.traceMarkerId = veaf.loggers.get(veafCarrierOperations.Id):markerArrow(veafCarrierOperations.traceMarkerId, "CARRIER", "route", startPosition, newWaypoint, veafCarrierOperations.getDebugMarkersErasedAtEachStep(carrier.carrierUnitName), VeafDrawingOnMap.LINE_TYPE["dashed"], {0, 0, 1, 0.3}) local actualSpeed = speed if math.abs(dir - currentHeading) > 15 then -- still aligning actualSpeed = veafCarrierOperations.ALIGNMENT_MANOEUVER_SPEED end veaf.moveGroupTo(groupName, newWaypoint, actualSpeed, 0) carrier.heading = dir veaf.loggers.get(veafCarrierOperations.Id):trace("carrier.heading = " .. carrier.heading .. " (true)") --carrier.heading_mag = dir + magdev --veaf.loggers.get(veafCarrierOperations.Id):trace("carrier.heading = " .. carrier.heading_mag .. " (mag)") carrier.speed = veaf.round(speed * 1.94384, 0) veaf.loggers.get(veafCarrierOperations.Id):trace("carrier.speed = " .. carrier.speed .. " kn") -- check if a Pedro group exists for this carrier if not(mist.DBs.groupsByName[carrier.pedroUnitName]) then veaf.loggers.get(veafCarrierOperations.Id):warn("No Pedro group named " .. carrier.pedroUnitName) else -- prepare or correct the Pedro route (SH-60B, 250ft high, 1nm to the starboard side of the carrier, riding along at the same speed and heading) local pedroUnit = Unit.getByName(carrier.pedroUnitName) if (pedroUnit) then veaf.loggers.get(veafCarrierOperations.Id):debug("found Pedro unit") -- check if unit is still alive if pedroUnit:getLife() < 1 then pedroUnit = nil -- respawn when damaged end end -- spawn if needed if not(pedroUnit and carrier.pedroIsSpawned) then veaf.loggers.get(veafCarrierOperations.Id):debug("respawning Pedro unit") local vars = {} vars.gpName = carrier.pedroUnitName vars.action = 'respawn' vars.point = startPosition vars.point.y = 100 vars.radius = 500 mist.teleportToPoint(vars) carrier.pedroIsSpawned = true end local pedroGroup = Group.getByName(carrier.pedroUnitName) -- group has the same name as the unit if (pedroGroup) then veaf.loggers.get(veafCarrierOperations.Id):debug("found Pedro group") pedroUnit = Unit.getByName(carrier.pedroUnitName) if not pedroUnit then pedroUnit = pedroGroup:getUnits(1) end veaf.loggers.get(veafCarrierOperations.Id):debug(string.format("pedroUnit=%s",veaf.p(pedroUnit))) -- waypoint #1 is 500m to port local offsetPointOnLand, offsetPoint = veaf.computeCoordinatesOffsetFromRoute(startPosition, newWaypoint, 0, 500) local pedroWaypoint1 = offsetPoint local distanceFromWP1 = ((pedroUnit:getPosition().p.x - pedroWaypoint1.x)^2 + (pedroUnit:getPosition().p.z - pedroWaypoint1.z)^2)^0.5 if distanceFromWP1 > 500 then veaf.loggers.get(veafCarrierOperations.Id):trace("Pedro WP1 = " .. veaf.vecToString(pedroWaypoint1)) veafCarrierOperations.traceMarkerId = veaf.loggers.get(veafCarrierOperations.Id):marker(veafCarrierOperations.traceMarkerId, "CARRIER", "pedroWaypoint1", pedroWaypoint1, veafCarrierOperations.getDebugMarkersErasedAtEachStep(carrier.carrierUnitName)) else pedroWaypoint1 = nil end -- waypoint #2 is 500m to port, near the end of the carrier route local offsetPointOnLand, offsetPoint = veaf.computeCoordinatesOffsetFromRoute(startPosition, newWaypoint, length - 250, 500) local pedroWaypoint2 = offsetPoint veaf.loggers.get(veafCarrierOperations.Id):trace("Pedro WP2 = " .. veaf.vecToString(pedroWaypoint2)) veafCarrierOperations.traceMarkerId = veaf.loggers.get(veafCarrierOperations.Id):marker(veafCarrierOperations.traceMarkerId, "CARRIER", "pedroWaypoint2", pedroWaypoint2, veafCarrierOperations.getDebugMarkersErasedAtEachStep(carrier.carrierUnitName)) local mission = { id = 'Mission', params = { ["communication"] = false, ["start_time"] = 0, ["task"] = "Transport", route = { points = { } } } } if pedroWaypoint1 then mission.params.route.points = { [1] = { ["alt"] = 35, ["action"] = "Turning Point", ["alt_type"] = "BARO", ["speed"] = 50, ["type"] = "Turning Point", ["x"] = pedroUnit:getPosition().p.x, ["y"] = pedroUnit:getPosition().p.z, ["speed_locked"] = true, }, [2] = { ["type"] = "Turning Point", ["action"] = "Turning Point", ["x"] = pedroWaypoint1.x, ["y"] = pedroWaypoint1.z, ["alt"] = 35, -- in meters ["alt_type"] = "BARO", ["speed"] = 50, ["speed_locked"] = true, }, [3] = { ["type"] = "Turning Point", ["action"] = "Turning Point", ["x"] = pedroWaypoint2.x, ["y"] = pedroWaypoint2.z, ["alt"] = 35, -- in meters ["alt_type"] = "BARO", ["speed"] = speed, -- speed in m/s ["speed_locked"] = true, }, } else mission.params.route.points = { [1] = { ["alt"] = 35, ["action"] = "Turning Point", ["alt_type"] = "BARO", ["speed"] = 50, ["type"] = "Turning Point", ["x"] = pedroUnit:getPosition().p.x, ["y"] = pedroUnit:getPosition().p.z, ["speed_locked"] = true, }, [2] = { ["type"] = "Turning Point", ["action"] = "Turning Point", ["x"] = pedroWaypoint2.x, ["y"] = pedroWaypoint2.z, ["alt"] = 35, -- in meters ["alt_type"] = "BARO", ["speed"] = speed, -- speed in m/s ["speed_locked"] = true, }, } end -- replace whole mission veaf.loggers.get(veafCarrierOperations.Id):debug("Setting Pedro mission") local controller = pedroGroup:getController() controller:setTask(mission) end end -- check if a S3B-Tanker group exists for this carrier if not(mist.DBs.groupsByName[carrier.tankerUnitName]) then veaf.loggers.get(veafCarrierOperations.Id):warn("No Tanker group named " .. carrier.tankerUnitName) else local routeTanker = (carrierDistanceFromInitialPosition > 18520) carrier.tankerRouteSet = carrier.tankerRouteSet + 1 if carrier.tankerRouteSet <= 2 then -- prepare or correct the Tanker route (8000ft high, 10nm aft and 4nm to the starboard side of the carrier, refueling on BRC) local tankerUnit = Unit.getByName(carrier.tankerUnitName) if (tankerUnit) then veaf.loggers.get(veafCarrierOperations.Id):debug("found Tanker unit") -- check if unit is still alive if tankerUnit:getLife() < 1 then tankerUnit = nil -- respawn when damaged end end -- spawn if needed if not(tankerUnit and carrier.tankerIsSpawned) then veaf.loggers.get(veafCarrierOperations.Id):debug("respawning Tanker unit") local vars = {} vars.gpName = carrier.tankerUnitName vars.action = 'respawn' vars.point = {} vars.point.x = startPosition.x vars.point.z = startPosition.z vars.point.y = 2500 vars.radius = 500 mist.teleportToPoint(vars) carrier.tankerIsSpawned = true end tankerUnit = Unit.getByName(carrier.tankerUnitName) local tankerGroup = Group.getByName(carrier.tankerUnitName) -- group has the same name as the unit if (tankerGroup) then veaf.loggers.get(veafCarrierOperations.Id):debug("found Tanker group") veaf.loggers.get(veafCarrierOperations.Id):trace("groupName="..tankerGroup:getName()) -- waypoint #1 is 5nm to port, 5nm to the front local offsetPointOnLand, offsetPoint = veaf.computeCoordinatesOffsetFromRoute(startPosition, newWaypoint, 9000, 9000) local tankerWaypoint1 = offsetPoint veaf.loggers.get(veafCarrierOperations.Id):trace("Tanker WP1 = " .. veaf.vecToString(tankerWaypoint1)) veafCarrierOperations.traceMarkerId = veaf.loggers.get(veafCarrierOperations.Id):marker(veafCarrierOperations.traceMarkerId, "CARRIER", "tankerWaypoint1", tankerWaypoint1, veafCarrierOperations.getDebugMarkersErasedAtEachStep(carrier.carrierUnitName)) -- waypoint #2 is 20nm ahead of waypoint #2, on BRC local offsetPointOnLand, offsetPoint = veaf.computeCoordinatesOffsetFromRoute(startPosition, newWaypoint, 37000 + 9000, 9000) local tankerWaypoint2 = offsetPoint veaf.loggers.get(veafCarrierOperations.Id):trace("Tanker WP2 = " .. veaf.vecToString(tankerWaypoint2)) veafCarrierOperations.traceMarkerId = veaf.loggers.get(veafCarrierOperations.Id):marker(veafCarrierOperations.traceMarkerId, "CARRIER", "tankerWaypoint2", tankerWaypoint2, veafCarrierOperations.getDebugMarkersErasedAtEachStep(carrier.carrierUnitName)) local mission = { id = 'Mission', params = { ["communication"] = true, ["start_time"] = 0, ["task"] = "Refueling", ["taskSelected"] = true, ["route"] = { ["points"] = { [1] = { ["alt"] = 2500, ["action"] = "Turning Point", ["alt_type"] = "BARO", ["speed"] = 165, ["type"] = "Turning Point", ["x"] = startPosition.x, ["y"] = startPosition.z, ["speed_locked"] = true, }, [2] = { ["alt"] = 2500, ["action"] = "Turning Point", ["alt_type"] = "BARO", ["speed"] = 165, ["task"] = { ["id"] = "ComboTask", ["params"] = { ["tasks"] = { [1] = { ["enabled"] = true, ["auto"] = true, ["id"] = "Tanker", ["number"] = 1, }, -- end of [1] [2] = carrier.tankerData.tankerTacanTask }, -- end of ["tasks"] }, -- end of ["params"] }, -- end of ["task"] ["type"] = "Turning Point", ["ETA"] = 0, ["ETA_locked"] = false, ["x"] = startPosition.x, ["y"] = startPosition.z, ["speed_locked"] = true, }, [3] = { ["alt"] = 2500, ["action"] = "Turning Point", ["alt_type"] = "BARO", ["speed"] = 165, ["task"] = { ["id"] = "ComboTask", ["params"] = { ["tasks"] = { [1] = { ["enabled"] = true, ["auto"] = false, ["id"] = "Orbit", ["number"] = 1, ["params"] = { ["altitude"] = 2500, ["pattern"] = "Race-Track", ["speed"] = 165, }, -- end of ["params"] }, -- end of [1] }, -- end of ["tasks"] }, -- end of ["params"] }, -- end of ["task"] ["type"] = "Turning Point", ["x"] = tankerWaypoint1.x, ["y"] = tankerWaypoint1.z, ["speed_locked"] = true, }, [4] = { ["alt"] = 2500, ["action"] = "Turning Point", ["alt_type"] = "BARO", ["speed"] = 165, ["type"] = "Turning Point", ["x"] = tankerWaypoint2.x, ["y"] = tankerWaypoint2.z, ["speed_locked"] = true, }, -- end of [3] }, -- end of ["points"] }, -- end of ["route"] } } -- replace whole mission veaf.loggers.get(veafCarrierOperations.Id):debug("Setting Tanker mission") local controller = tankerGroup:getController() controller:setTask(mission) carrier.tankerRouteIsSet = true local _setFrequency = { id = 'SetFrequency', params = { frequency = carrier.tankerData.tankerFrequency * 1000000, --Hz modulation = 0 --AM } } Controller.setCommand(controller, _setFrequency) end end end end end --- Gets informations about current carrier operations function veafCarrierOperations.getAtcForCarrierOperations(groupName, skipNavigationData) veaf.loggers.get(veafCarrierOperations.Id):debug("getAtcForCarrierOperations(".. groupName .. ")") local carrier = veafCarrierOperations.carriers[groupName] local carrierUnit = Unit.getByName(carrier.carrierUnitName) local currentHeading = -1 local currentSpeed = -1 local startPosition = nil if carrierUnit then currentHeading = mist.utils.round(mist.utils.toDegree(mist.getHeading(carrierUnit, true)), 0) currentSpeed = mist.utils.round(mist.utils.mpsToKnots(mist.vec.mag(carrierUnit:getVelocity())),0) startPosition = { x=carrierUnit:getPosition().p.x, z=carrierUnit:getPosition().p.z, y=veafCarrierOperations.ALT_FOR_MEASURING_WIND} -- on deck, 50 meters above the water end if not(carrier) then local text = "Cannot find the carrier group "..groupName veaf.loggers.get(veafCarrierOperations.Id):error(text) trigger.action.outText(text, 5) return end local result = "" local groupPosition = veaf.getAvgGroupPos(groupName) if carrier.conductingAirOperations then local remainingTime = veaf.round((carrier.airOperationsEndAt - timer.getTime()) /60, 1) result = "The carrier group "..groupName.." is conducting air operations :\n" if carrier.ATC.tower then result = result .. " - ATC : " .. carrier.ATC.tower .. "\n" end if carrier.ATC.tacan then result = result .. " - TACAN : " .. carrier.ATC.tacan .. "\n" end if carrier.ATC.icls then result = result .. " - ICLS : " .. carrier.ATC.icls .. "\n" end if carrier.ATC.link4 then result = result .. " - LINK 4 : " .. carrier.ATC.link4 .. ", " if carrier.ATC.acls then result = result .. "ACLS is available" end result = result .. "\n" end --" - BRC : " .. carrier.heading_mag .. " (".. carrier.heading .. " true) at " .. carrier.speed .. " kn\n" .. result = result .. "\n - BRC : " .. carrier.heading .. " (true) at " .. carrier.speed .. " kn\n" .. " - Remaining time : " .. remainingTime .. " minutes\n" if carrier.tankerData then result = result .. "\n - Tanker " .. carrier.tankerData.tankerCallsign .. " : TACAN " ..carrier.tankerData.tankerTacanChannel.. carrier.tankerData.tankerTacanMode ..", COMM " .. carrier.tankerData.tankerFrequency .. "\n" end else result = "The carrier group "..groupName.." is not conducting carrier air operations\n" end if not(skipNavigationData) then -- add current navigation data if currentHeading > -1 and currentSpeed > -1 then -- compute magnetic deviation at carrier position -- let's not use mist.getNorthCorrection, it's not computing magnetic deviation... -- TODO find how to actually compute it --[[ local magdev = veaf.round(mist.getNorthCorrection(startPosition) * 180 / math.pi,1) veaf.loggers.get(veafCarrierOperations.Id):trace("magdev = " .. magdev) ]] result = result .. "\n".. "Current navigation parameters :\n" .. " - Current heading (true) " .. veaf.round(currentHeading, 0) .. "\n" .. --" - Current heading (mag) " .. veaf.round(currentHeading, 0) .. "\n" .. " - Current speed " .. currentSpeed .. " kn\n" end end result = result .. "\n"..veaf.weatherReport(startPosition) return result end --- Gets informations about current carrier operations function veafCarrierOperations.atcForCarrierOperations(parameters) local groupName, unitName = veaf.safeUnpack(parameters) veaf.loggers.get(veafCarrierOperations.Id):debug("atcForCarrierOperations(".. groupName .. ")") local text = veafCarrierOperations.getAtcForCarrierOperations(groupName) veaf.outTextForGroup(unitName, text, 15) end --- Ends carrier operations ; changes the radio menu item to START and send the carrier back to its starting point function veafCarrierOperations.stopCarrierOperations(parameters) local groupName, userUnitName = veaf.safeUnpack(parameters) veaf.loggers.get(veafCarrierOperations.Id):debug("stopCarrierOperations(".. groupName .. ")") local carrier = veafCarrierOperations.carriers[groupName] if not(carrier) then local text = "Cannot find the carrier group "..groupName veaf.loggers.get(veafCarrierOperations.Id):error(text) trigger.action.outText(text, 5) return end local carrierUnit = Unit.getByName(carrier.carrierUnitName) local carrierPosition = carrierUnit:getPosition().p local text = "The carrier group "..groupName.." has stopped air operations ; it's moving back to its initial position" veaf.loggers.get(veafCarrierOperations.Id):info(text) veaf.outTextForGroup(userUnitName, text, 5) carrier.conductingAirOperations = false carrier.stoppedAirOperations = true -- change the menu veaf.loggers.get(veafCarrierOperations.Id):trace("change the menu") veafCarrierOperations.rebuildRadioMenu() -- make the Pedro land if (carrier.pedroIsSpawned) then carrier.pedroIsSpawned = false local pedroUnit = Unit.getByName(carrier.pedroUnitName) if (pedroUnit) then veaf.loggers.get(veafCarrierOperations.Id):debug("found Pedro unit ; destroying it") pedroUnit:destroy() end end -- make the tanker land if (carrier.tankerIsSpawned) then carrier.tankerIsSpawned = false local tankerUnit = Unit.getByName(carrier.tankerUnitName) if (tankerUnit) then veaf.loggers.get(veafCarrierOperations.Id):debug("found tanker unit ; destroying it") tankerUnit:destroy() end end veafCarrierOperations.doOperations() end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Radio menu and help ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Rebuild the radio menu function veafCarrierOperations.rebuildRadioMenu() veaf.loggers.get(veafCarrierOperations.Id):debug("veafCarrierOperations.rebuildRadioMenu()") -- find the carriers in the veafCarrierOperations.carriers table and prepare their menus for name, carrier in pairs(veafCarrierOperations.carriers) do veaf.loggers.get(veafCarrierOperations.Id):trace("rebuildRadioMenu processing "..name) local menuRoot = veafCarrierOperations.rootPathRed if carrier.side == coalition.side.BLUE then menuRoot = veafCarrierOperations.rootPathBlue end -- remove the submenu if it exists if carrier.menuPath then veafRadio.delSubmenu(carrier.menuPath, menuRoot) veaf.loggers.get(veafCarrierOperations.Id):trace("remove the submenu") end -- create the submenu veaf.loggers.get(veafCarrierOperations.Id):trace("create the submenu") carrier.menuPath = veafRadio.addSubMenu(name, menuRoot) if carrier.conductingAirOperations then -- add the stop menu if veafCarrierOperations.DisableSecurity then veafRadio.addCommandToSubmenu("End air operations", carrier.menuPath, veafCarrierOperations.stopCarrierOperations, name, veafRadio.USAGE_ForGroup) else veafRadio.addSecuredCommandToSubmenu("End air operations", carrier.menuPath, veafCarrierOperations.stopCarrierOperations, name, veafRadio.USAGE_ForGroup) end else -- add the "start for veafCarrierOperations.MAX_OPERATIONS_DURATION" menu local startMenuName1 = "Start carrier air operations for " .. veafCarrierOperations.MAX_OPERATIONS_DURATION .. " minutes" if veafCarrierOperations.DisableSecurity then veafRadio.addCommandToSubmenu(startMenuName1, carrier.menuPath, veafCarrierOperations.startCarrierOperations, { name, veafCarrierOperations.MAX_OPERATIONS_DURATION }, veafRadio.USAGE_ForGroup) else veafRadio.addSecuredCommandToSubmenu(startMenuName1, carrier.menuPath, veafCarrierOperations.startCarrierOperations, { name, veafCarrierOperations.MAX_OPERATIONS_DURATION }, veafRadio.USAGE_ForGroup) end -- add the "start for veafCarrierOperations.MAX_OPERATIONS_DURATION * 2" menu local startMenuName2 = "Start carrier air operations for " .. veafCarrierOperations.MAX_OPERATIONS_DURATION * 2 .. " minutes" if veafCarrierOperations.DisableSecurity then veafRadio.addCommandToSubmenu(startMenuName2, carrier.menuPath, veafCarrierOperations.startCarrierOperations, { name, veafCarrierOperations.MAX_OPERATIONS_DURATION * 2 }, veafRadio.USAGE_ForGroup) else veafRadio.addSecuredCommandToSubmenu(startMenuName2, carrier.menuPath, veafCarrierOperations.startCarrierOperations, { name, veafCarrierOperations.MAX_OPERATIONS_DURATION * 2 }, veafRadio.USAGE_ForGroup) end end -- add the ATC menu (by player group) veafRadio.addCommandToSubmenu("ATC - Request informations", carrier.menuPath, veafCarrierOperations.atcForCarrierOperations, name, veafRadio.USAGE_ForGroup) veafRadio.refreshRadioMenu() end end --- Build the initial radio menu function veafCarrierOperations.buildRadioMenu() veaf.loggers.get(veafCarrierOperations.Id):debug("veafCarrierOperations.buildRadioMenu") -- don't create an empty menu if veaf.length(veafCarrierOperations.carriers) == 0 then return end veafCarrierOperations.rootPath = veafRadio.addSubMenu(veafCarrierOperations.RadioMenuName) veafCarrierOperations.rootPathBlue = veafRadio.addSubMenu(veafCarrierOperations.RadioMenuNameBlue, veafCarrierOperations.rootPath) veafCarrierOperations.rootPathRed = veafRadio.addSubMenu(veafCarrierOperations.RadioMenuNameRed, veafCarrierOperations.rootPath) -- build HELP menu for each group if not(veafRadio.skipHelpMenus) then veafRadio.addCommandToSubmenu("HELP", veafCarrierOperations.rootPath, veafCarrierOperations.help, nil, veafRadio.USAGE_ForGroup) end veafCarrierOperations.rebuildRadioMenu() end function veafCarrierOperations.help(unitName) local text = 'Use the radio menus to start and end carrier operations\n' .. 'START: carrier will find out the wind and set sail at optimum speed to achieve a 25kn headwind\n' .. ' the radio menu will show the recovery course and TACAN information\n' .. 'END : carrier will go back to its starting point (where it was when the START command was issued)\n' .. 'RESET: carrier will go back to where it was when the mission started' veaf.outTextForGroup(unitName, text, 30) end function veafCarrierOperations.initializeCarrierGroups() -- find the carriers and add them to the veafCarrierOperations.carriers table, store its initial location and create the menus for name, group in pairs(mist.DBs.groupsByName) do veaf.loggers.get(veafCarrierOperations.Id):trace("found group "..name) -- search groups with a carrier unit in the group local carrier = nil -- find the actual carrier unit local group = Group.getByName(name) if group then for _, unit in pairs(group:getUnits()) do local unitType = unit:getDesc()["typeName"] for knownCarrierType, data in pairs(veafCarrierOperations.AllCarriers) do if unitType == knownCarrierType then local coa = group:getCoalition() veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("coa=%s", veaf.p(coa))) -- found a carrier, initialize the carrier group object if needed if not carrier then veafCarrierOperations.carriers[name] = {} carrier = veafCarrierOperations.carriers[name] veaf.loggers.get(veafCarrierOperations.Id):trace("found carrier !") else veaf.loggers.get(veafCarrierOperations.Id):warn(string.format("more than one carrier in group %s", veaf.p(name))) end carrier.side = coa carrier.carrierUnit = unit carrier.carrierUnitName = carrier.carrierUnit:getName() carrier.runwayAngleWithBRC = data.runwayAngleWithBRC carrier.desiredWindSpeedOnDeck = data.desiredWindSpeedOnDeck carrier.heading = mist.getHeading(unit, true) --veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("Carrier Data from MIST : %s",veaf.p(veaf.getGroupData(name)))) carrier.ATC = {} carrier.ATC = veaf.getCarrierATCdata(name, unit:getName()) carrier.pedroUnitName = carrier.carrierUnitName .. " Pedro" -- rescue helo unit name local pedroUnit = Unit.getByName(carrier.pedroUnitName) if pedroUnit then pedroUnit:destroy() end carrier.tankerUnitName = carrier.carrierUnitName .. " S3B-Tanker" -- emergency tanker unit name carrier.tankerData = veaf.getTankerData(carrier.tankerUnitName) local tankerUnit = Unit.getByName(carrier.tankerUnitName) if tankerUnit then tankerUnit:destroy() end break end end end if carrier then -- take note of the carrier route carrier.missionRoute = mist.getGroupRoute(name, 'task') veaf.loggers.get(veafCarrierOperations.Id):trace("carrier.missionRoute=%s", veaf.p(carrier.missionRoute)) if veafCarrierOperations.Trace then for num, point in pairs(carrier.missionRoute) do veafCarrierOperations.traceMarkerId = veaf.loggers.get(veafCarrierOperations.Id):marker(veafCarrierOperations.traceMarkerId, "CARRIER", string.format("[%s] point %d", name, tostring(num)), point, nil) end end end end end end function veafCarrierOperations.doOperations() veaf.loggers.get(veafCarrierOperations.Id):debug("veafCarrierOperations.doOperations()") -- find the carriers in the veafCarrierOperations.carriers table and check if they are operating for name, carrier in pairs(veafCarrierOperations.carriers) do veaf.loggers.get(veafCarrierOperations.Id):debug("checking " .. name) if carrier.conductingAirOperations then veaf.loggers.get(veafCarrierOperations.Id):debug(name .. " is conducting operations ; checking course and ops duration") if carrier.airOperationsEndAt < timer.getTime() then -- time to stop operations veaf.loggers.get(veafCarrierOperations.Id):info(name .. " has been conducting operations long enough ; stopping ops") veafCarrierOperations.stopCarrierOperations(name) else local remainingTime = veaf.round((carrier.airOperationsEndAt - timer.getTime()) /60, 1) veaf.loggers.get(veafCarrierOperations.Id):debug(name .. " will continue conducting operations for " .. remainingTime .. " more minutes") -- check and reset course veafCarrierOperations.continueCarrierOperations(name) end elseif carrier.stoppedAirOperations then carrier.conductingAirOperations = false veaf.loggers.get(veafCarrierOperations.Id):debug(name .. " stopped conducting operations") veaf.loggers.get(veafCarrierOperations.Id):cleanupMarkers(veafCarrierOperations.getDebugMarkersErasedAtEachStep(carrier.carrierUnitName)) carrier.stoppedAirOperations = false -- reset the carrier group route to its original route (set in the mission) if carrier.missionRoute then veaf.loggers.get(veafCarrierOperations.Id):debug(string.format("resetting carrier %s route", name)) veaf.loggers.get(veafCarrierOperations.Id):trace("carrier.missionRoute="..veaf.p(carrier.missionRoute)) local result = mist.goRoute(name, carrier.missionRoute) end else veaf.loggers.get(veafCarrierOperations.Id):debug(name .. " is not conducting operations") end end end --- This function is called at regular interval (see veafCarrierOperations.SCHEDULER_INTERVAL) and manages the carrier operations schedules --- It will make any carrier group that has started carrier operations maintain a correct course for recovery, even if wind changes. --- Also, it will stop carrier operations after a set time (see veafCarrierOperations.MAX_OPERATIONS_DURATION). function veafCarrierOperations.operationsScheduler() veaf.loggers.get(veafCarrierOperations.Id):debug("veafCarrierOperations.operationsScheduler()") veafCarrierOperations.doOperations() veaf.loggers.get(veafCarrierOperations.Id):debug("veafCarrierOperations.operationsScheduler() - rescheduling in " .. veafCarrierOperations.SCHEDULER_INTERVAL * 60 .. " s") mist.scheduleFunction(veafCarrierOperations.operationsScheduler,{},timer.getTime() + veafCarrierOperations.SCHEDULER_INTERVAL * 60) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- remote interface ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafCarrierOperations.listAvailableCarriers(forGroup) local _message = "Available carriers :\n" for name, carrier in pairs(veafCarrierOperations.carriers) do _message = _message .. " - " .. name .. "\n" end if forGroup then trigger.action.outTextForGroup(forGroup, _message, 15) else trigger.action.outText(_message, 15) end end -- execute command from the remote interface function veafCarrierOperations.executeCommandFromRemote(parameters) veaf.loggers.get(veafCarrierOperations.Id):debug(string.format("veafCarrierOperations.executeCommandFromRemote()")) veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("parameters= %s", veaf.p(parameters))) local _pilot, _pilotName, _unitName, _command = unpack(parameters) veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("_pilot= %s", veaf.p(_pilot))) veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("_pilotName= %s", veaf.p(_pilotName))) veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("_unitName= %s", veaf.p(_unitName))) veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("_command= %s", veaf.p(_command))) if not _pilot or not _command then return false end local function findCarrier(carrierName) local _result = nil local _name = carrierName:lower() for name, carrier in pairs(veafCarrierOperations.carriers) do if name:lower():find(_name) then _result = name end end return _result end if _command then -- parse the command local _action, _carrierName, _parameters = _command:match(veafCarrierOperations.RemoteCommandParser) veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("_action=%s",veaf.p(_action))) veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("_carrierName=%s",veaf.p(_carrierName))) veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("_parameters=%s",veaf.p(_parameters))) local _groupId = nil if _unitName then local _unit = Unit.getByName(_unitName) if _unit then _groupId = _unit:getGroup():getID() end end veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("_groupId=%s",veaf.p(_groupId))) if _action and _action:lower() == "list" then veaf.loggers.get(veafCarrierOperations.Id):info(string.format("[%s] is listing carriers)",veaf.p(_pilot.name))) veafCarrierOperations.listAvailableCarriers(_groupId) return true elseif _action and _action:lower() == "start" and _carrierName then local _duration = 45 if _parameters and type(_parameters) == "number" then _duration = tonumber(parameters) end local _carrier = findCarrier(_carrierName) veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("_duration=%s",veaf.p(_duration))) veaf.loggers.get(veafCarrierOperations.Id):info(string.format("[%s] is starting operations on carrier [%s] for %s)",veaf.p(_pilot.name), veaf.p(_carrier), veaf.p(_parameters))) veafCarrierOperations.startCarrierOperations({_carrier, _duration}) return true elseif _action and _action:lower() == "stop" then local _carrier = findCarrier(_carrierName) veaf.loggers.get(veafCarrierOperations.Id):info(string.format("[%s] is stopping operations on carrier [%s])",veaf.p(_pilot.name), veaf.p(_carrier))) veafCarrierOperations.stopCarrierOperations(_carrier) return true elseif _action and _action:lower() == "atc" then local _carrier = findCarrier(_carrierName) veaf.loggers.get(veafCarrierOperations.Id):info(string.format("[%s] is requesting atc on carrier [%s])",veaf.p(_pilot.name), veaf.p(_carrier))) local text = veafCarrierOperations.getAtcForCarrierOperations(_carrier) if _groupId then trigger.action.outTextForGroup(_groupId, text, 15) else trigger.action.outText(text, 15) end return true end end return false end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Carrier ATC ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafCarrierOperations.initialize() veafCarrierOperations.initializeCarrierGroups() veafCarrierOperations.buildRadioMenu() veafCarrierOperations.operationsScheduler() end veaf.loggers.get(veafCarrierOperations.Id):info(string.format("Loading version %s", veafCarrierOperations.Version)) --- Enable/Disable error boxes displayed on screen. env.setErrorMessageBoxEnabled(false)