--[[ Simple AFAC System v1.0 ======================== A lightweight, standalone Forward Air Controller system for DCS World. No external dependencies required. ]] -- Initialize with success message trigger.action.outText("Simple AFAC System v1.0 loading...", 10) -- ===================================================== -- CONFIGURATION & INITIALIZATION -- ===================================================== AFAC = {} AFAC.Config = { maxRange = 18520, laserCodes = {'1688', '1677', '1666', '1113', '1115', '1111'}, smokeColors = {GREEN = 0, RED = 1, WHITE = 2, ORANGE = 3, BLUE = 4}, defaultSmokeColor = {[1] = 4, [2] = 3}, -- RED uses BLUE, BLUE uses ORANGE mapMarkerDuration = 120, smokeInterval = 300, autoUpdateInterval = 1.0, debug = true, afacAircraft = { "UH-1H", "UH-60L", "SA342L", "SA342Mistral", "SA342Minigun", "OH-58D", "Mi-8MT", "CH-47F", "P-51D", "A-4E-C", "L-39C", "C-101CC" } } AFAC.Data = { pilots = {}, targets = {}, laserPoints = {}, irPoints = {}, smokeMarks = {}, onStation = {}, laserCodes = {}, markerSettings = {}, manualTargets = {}, menuIds = {}, nextMarkerId = 1000 } -- ===================================================== -- UTILITY FUNCTIONS -- ===================================================== function AFAC.log(message) if AFAC.Config.debug then env.info("AFAC: " .. tostring(message)) end end function AFAC.contains(table, value) for i = 1, #table do if table[i] == value then return true end end return false end function AFAC.getDistance(point1, point2) local dx = point1.x - point2.x local dz = point1.z - point2.z return math.sqrt(dx * dx + dz * dz) end function AFAC.getGroupId(unit) local group = unit:getGroup() if group then return group:getID() end return nil end function AFAC.notifyCoalition(message, duration, coalition) trigger.action.outTextForCoalition(coalition, message, duration or 10) end -- ===================================================== -- AIRCRAFT DETECTION -- ===================================================== function AFAC.isAFAC(unit) if not unit then return false end local unitType = unit:getTypeName() AFAC.log("Checking aircraft type: " .. unitType) local result = AFAC.contains(AFAC.Config.afacAircraft, unitType) AFAC.log("isAFAC result for " .. unitType .. ": " .. tostring(result)) return result end function AFAC.addPilot(unit) local unitName = unit:getName() local groupId = AFAC.getGroupId(unit) if not groupId then return end -- Check if pilot is already registered if AFAC.Data.pilots[unitName] then AFAC.log("Pilot " .. unitName .. " already registered, skipping") return end AFAC.log("Adding AFAC pilot: " .. unitName) -- Initialize pilot data AFAC.Data.pilots[unitName] = { name = unitName, unit = unit, coalition = unit:getCoalition(), groupId = groupId } -- Set defaults AFAC.Data.laserCodes[unitName] = AFAC.Config.laserCodes[1] AFAC.Data.markerSettings[unitName] = { type = "SMOKE", color = AFAC.Config.defaultSmokeColor[unit:getCoalition()] } AFAC.Data.onStation[unitName] = true -- Notify player local message = string.format("AFAC System Active\nAircraft: %s\nUse F10 menu to control targeting", unit:getTypeName()) trigger.action.outTextForGroup(groupId, message, 15) -- Add F10 menu with slight delay to ensure everything is initialized timer.scheduleFunction(function(args) AFAC.addMenus(args[1]) end, {unitName}, timer.getTime() + 0.5) -- Start auto-lasing AFAC.startAutoLasing(unitName) end function AFAC.removePilot(unitName) if not AFAC.Data.pilots[unitName] then return end AFAC.cancelLasing(unitName) -- Clean up data AFAC.Data.pilots[unitName] = nil AFAC.Data.targets[unitName] = nil AFAC.Data.laserPoints[unitName] = nil AFAC.Data.irPoints[unitName] = nil AFAC.Data.smokeMarks[unitName] = nil AFAC.Data.onStation[unitName] = nil AFAC.Data.laserCodes[unitName] = nil AFAC.Data.markerSettings[unitName] = nil AFAC.Data.manualTargets[unitName] = nil AFAC.Data.menuIds[unitName] = nil AFAC.log("Removed AFAC pilot: " .. unitName) end -- ===================================================== -- TARGET DETECTION -- ===================================================== function AFAC.findNearestTarget(afacUnit) local afacPoint = afacUnit:getPoint() local afacCoalition = afacUnit:getCoalition() local enemyCoalition = afacCoalition == 1 and 2 or 1 local nearestTarget = nil local nearestDistance = AFAC.Config.maxRange local searchVolume = { id = world.VolumeType.SPHERE, params = {point = afacPoint, radius = nearestDistance} } local function checkUnit(foundUnit) if foundUnit:getCoalition() ~= enemyCoalition then return end if not foundUnit:isActive() then return end if foundUnit:inAir() then return end if foundUnit:getLife() <= 1 then return end local unitPoint = foundUnit:getPoint() local distance = AFAC.getDistance(afacPoint, unitPoint) if distance >= nearestDistance then return end -- Check line of sight local offsetAfacPos = {x = afacPoint.x, y = afacPoint.y + 2, z = afacPoint.z} local offsetUnitPos = {x = unitPoint.x, y = unitPoint.y + 2, z = unitPoint.z} if not land.isVisible(offsetAfacPos, offsetUnitPos) then return end -- Priority system local priority = 1 if foundUnit:hasAttribute("SAM TR") or foundUnit:hasAttribute("IR Guided SAM") then priority = 0.1 elseif foundUnit:hasAttribute("Vehicles") then priority = 0.5 end local adjustedDistance = distance * priority if adjustedDistance < nearestDistance then nearestDistance = adjustedDistance nearestTarget = foundUnit end end world.searchObjects(Object.Category.UNIT, searchVolume, checkUnit) return nearestTarget end -- ===================================================== -- LASER DESIGNATION -- ===================================================== function AFAC.startLasing(afacUnit, target, laserCode) local unitName = afacUnit:getName() AFAC.cancelLasing(unitName) local targetPoint = target:getPoint() local targetVector = {x = targetPoint.x, y = targetPoint.y + 2, z = targetPoint.z} local success, result = pcall(function() local laserSpot = Spot.createLaser(afacUnit, {x = 0, y = 2, z = 0}, targetVector, laserCode) local irSpot = Spot.createInfraRed(afacUnit, {x = 0, y = 2, z = 0}, targetVector) return {laser = laserSpot, ir = irSpot} end) if success and result then AFAC.Data.laserPoints[unitName] = result.laser AFAC.Data.irPoints[unitName] = result.ir AFAC.log("Started lasing target for " .. unitName) end end function AFAC.updateLasing(unitName, target) local laserSpot = AFAC.Data.laserPoints[unitName] local irSpot = AFAC.Data.irPoints[unitName] if not laserSpot or not irSpot then return end local targetPoint = target:getPoint() local targetVector = {x = targetPoint.x, y = targetPoint.y + 2, z = targetPoint.z} laserSpot:setPoint(targetVector) irSpot:setPoint(targetVector) end function AFAC.cancelLasing(unitName) local laserSpot = AFAC.Data.laserPoints[unitName] local irSpot = AFAC.Data.irPoints[unitName] if laserSpot then Spot.destroy(laserSpot) AFAC.Data.laserPoints[unitName] = nil end if irSpot then Spot.destroy(irSpot) AFAC.Data.irPoints[unitName] = nil end end -- ===================================================== -- VISUAL MARKING -- ===================================================== function AFAC.createVisualMarker(target, markerType, color) local targetPoint = target:getPoint() local markerPoint = {x = targetPoint.x, y = targetPoint.y + 2, z = targetPoint.z} if markerType == "SMOKE" then trigger.action.smoke(markerPoint, color) else trigger.action.signalFlare(markerPoint, color, 0) end end function AFAC.createMapMarker(target, spotter) local targetPoint = target:getPoint() local coalition = AFAC.Data.pilots[spotter].coalition local markerText = string.format("%s\nSpotter: %s", target:getTypeName(), spotter) local markerId = AFAC.Data.nextMarkerId AFAC.Data.nextMarkerId = AFAC.Data.nextMarkerId + 1 trigger.action.markToCoalition(markerId, markerText, targetPoint, coalition, false, "AFAC Target") timer.scheduleFunction(function(args) trigger.action.removeMark(args[1]) end, {markerId}, timer.getTime() + AFAC.Config.mapMarkerDuration) return markerId end -- ===================================================== -- AUTO-LASING SYSTEM -- ===================================================== function AFAC.startAutoLasing(unitName) timer.scheduleFunction(AFAC.autoLaseUpdate, {unitName}, timer.getTime() + 1) end function AFAC.autoLaseUpdate(args) local unitName = args[1] local pilot = AFAC.Data.pilots[unitName] if not pilot or not AFAC.Data.onStation[unitName] then return end local afacUnit = pilot.unit if not afacUnit or not afacUnit:isActive() or afacUnit:getLife() <= 0 then AFAC.removePilot(unitName) return end local currentTarget = AFAC.Data.targets[unitName] local laserCode = AFAC.Data.laserCodes[unitName] local markerSettings = AFAC.Data.markerSettings[unitName] -- Check if current target is still valid if currentTarget and (not currentTarget:isActive() or currentTarget:getLife() <= 1) then local message = string.format("[%s] Target %s destroyed. Good job! Scanning for new targets.", unitName, currentTarget:getTypeName()) AFAC.notifyCoalition(message, 10, pilot.coalition) AFAC.Data.targets[unitName] = nil AFAC.cancelLasing(unitName) currentTarget = nil end -- Find new target if needed if not currentTarget then local newTarget = AFAC.findNearestTarget(afacUnit) if newTarget then AFAC.Data.targets[unitName] = newTarget AFAC.startLasing(afacUnit, newTarget, laserCode) AFAC.createVisualMarker(newTarget, markerSettings.type, markerSettings.color) AFAC.createMapMarker(newTarget, unitName) local message = string.format("[%s] Lasing new target: %s, CODE: %s", unitName, newTarget:getTypeName(), laserCode) AFAC.notifyCoalition(message, 10, pilot.coalition) currentTarget = newTarget end end -- Update laser position if we have a target if currentTarget then AFAC.updateLasing(unitName, currentTarget) -- Update smoke markers periodically local nextSmokeTime = AFAC.Data.smokeMarks[unitName] if not nextSmokeTime or nextSmokeTime < timer.getTime() then AFAC.createVisualMarker(currentTarget, markerSettings.type, markerSettings.color) AFAC.Data.smokeMarks[unitName] = timer.getTime() + AFAC.Config.smokeInterval end end -- Schedule next update timer.scheduleFunction(AFAC.autoLaseUpdate, args, timer.getTime() + AFAC.Config.autoUpdateInterval) end -- ===================================================== -- F10 MENU SYSTEM -- ===================================================== function AFAC.addMenus(unitName) local pilot = AFAC.Data.pilots[unitName] if not pilot then AFAC.log("addMenus: No pilot data found for " .. unitName) return end local groupId = pilot.groupId if not groupId then AFAC.log("addMenus: No group ID found for " .. unitName) return end -- Check if menus already exist for this pilot if AFAC.Data.menuIds[unitName] then AFAC.log("Menus already exist for " .. unitName .. ", skipping creation") return end AFAC.log("Creating menus for " .. unitName .. " (Group ID: " .. groupId .. ")") local mainMenu = missionCommands.addSubMenuForGroup(groupId, "AFAC Control") AFAC.Data.menuIds[unitName] = mainMenu AFAC.log("Main menu created") -- Wrap menu creation in pcall to catch any errors local success, error = pcall(function() -- Targeting mode local targetMenu = missionCommands.addSubMenuForGroup(groupId, "Targeting Mode", mainMenu) missionCommands.addCommandForGroup(groupId, "Auto Mode ON", targetMenu, AFAC.setAutoMode, {unitName, true}) missionCommands.addCommandForGroup(groupId, "Auto Mode OFF", targetMenu, AFAC.setAutoMode, {unitName, false}) AFAC.log("Targeting mode menu created") -- Laser codes local laserMenu = missionCommands.addSubMenuForGroup(groupId, "Laser Codes", mainMenu) for _, code in ipairs(AFAC.Config.laserCodes) do missionCommands.addCommandForGroup(groupId, "Code: " .. code, laserMenu, AFAC.setLaserCode, {unitName, code}) end AFAC.log("Laser codes menu created with " .. #AFAC.Config.laserCodes .. " codes") -- Marker settings local markerMenu = missionCommands.addSubMenuForGroup(groupId, "Marker Settings", mainMenu) local smokeMenu = missionCommands.addSubMenuForGroup(groupId, "Smoke Color", markerMenu) -- Add smoke colors missionCommands.addCommandForGroup(groupId, "GREEN", smokeMenu, AFAC.setMarkerColor, {unitName, "SMOKE", 0}) missionCommands.addCommandForGroup(groupId, "RED", smokeMenu, AFAC.setMarkerColor, {unitName, "SMOKE", 1}) missionCommands.addCommandForGroup(groupId, "WHITE", smokeMenu, AFAC.setMarkerColor, {unitName, "SMOKE", 2}) missionCommands.addCommandForGroup(groupId, "ORANGE", smokeMenu, AFAC.setMarkerColor, {unitName, "SMOKE", 3}) missionCommands.addCommandForGroup(groupId, "BLUE", smokeMenu, AFAC.setMarkerColor, {unitName, "SMOKE", 4}) AFAC.log("Marker settings menu created") -- Status missionCommands.addCommandForGroup(groupId, "AFAC Status", mainMenu, AFAC.showStatus, {unitName}) AFAC.log("All menus created successfully for " .. unitName) end) if not success then AFAC.log("Error creating menus for " .. unitName .. ": " .. tostring(error)) end end -- ===================================================== -- F10 MENU FUNCTIONS -- ===================================================== function AFAC.setAutoMode(args) local unitName = args[1] local autoMode = args[2] local pilot = AFAC.Data.pilots[unitName] if not pilot then return end AFAC.Data.onStation[unitName] = autoMode if autoMode then trigger.action.outTextForGroup(pilot.groupId, "Auto targeting mode enabled", 10) AFAC.startAutoLasing(unitName) else trigger.action.outTextForGroup(pilot.groupId, "Auto targeting mode disabled", 10) AFAC.cancelLasing(unitName) AFAC.Data.targets[unitName] = nil end end function AFAC.setLaserCode(args) local unitName = args[1] local laserCode = args[2] local pilot = AFAC.Data.pilots[unitName] if not pilot then return end AFAC.Data.laserCodes[unitName] = laserCode trigger.action.outTextForGroup(pilot.groupId, "Laser code set to: " .. laserCode, 10) local currentTarget = AFAC.Data.targets[unitName] if currentTarget then AFAC.startLasing(pilot.unit, currentTarget, laserCode) end end function AFAC.setMarkerColor(args) local unitName = args[1] local markerType = args[2] local color = args[3] local pilot = AFAC.Data.pilots[unitName] if not pilot then return end AFAC.Data.markerSettings[unitName] = {type = markerType, color = color} local colorNames = {"GREEN", "RED", "WHITE", "ORANGE", "BLUE"} trigger.action.outTextForGroup(pilot.groupId, string.format("Marker set to %s %s", colorNames[color + 1] or "UNKNOWN", markerType), 10) end function AFAC.showStatus(args) local unitName = args[1] local pilot = AFAC.Data.pilots[unitName] if not pilot then return end local status = "AFAC STATUS:\n\n" for pilotName, pilotData in pairs(AFAC.Data.pilots) do if pilotData.coalition == pilot.coalition then local target = AFAC.Data.targets[pilotName] local laserCode = AFAC.Data.laserCodes[pilotName] local onStation = AFAC.Data.onStation[pilotName] if target and target:isActive() then status = status .. string.format("%s: Targeting %s, CODE: %s\n", pilotName, target:getTypeName(), laserCode) elseif onStation then status = status .. string.format("%s: On station, searching for targets\n", pilotName) else status = status .. string.format("%s: Off station\n", pilotName) end end end trigger.action.outTextForGroup(pilot.groupId, status, 30) end -- ===================================================== -- EVENT HANDLER -- ===================================================== AFAC.EventHandler = {} function AFAC.EventHandler:onEvent(event) if event.id == world.event.S_EVENT_BIRTH then local unit = event.initiator if unit and Object.getCategory(unit) == Object.Category.UNIT then local objDesc = unit:getDesc() if objDesc.category == Unit.Category.AIRPLANE or objDesc.category == Unit.Category.HELICOPTER then if AFAC.isAFAC(unit) then timer.scheduleFunction(function(args) local u = args[1] if u and u:isActive() then AFAC.addPilot(u) end end, {unit}, timer.getTime() + 2) end end end end if event.id == world.event.S_EVENT_PLAYER_ENTER_UNIT then local unit = event.initiator if unit and Object.getCategory(unit) == Object.Category.UNIT then local objDesc = unit:getDesc() if objDesc.category == Unit.Category.AIRPLANE or objDesc.category == Unit.Category.HELICOPTER then if AFAC.isAFAC(unit) then local unitName = unit:getName() if not AFAC.Data.pilots[unitName] then timer.scheduleFunction(function(args) local u = args[1] if u and u:isActive() then AFAC.addPilot(u) end end, {unit}, timer.getTime() + 1) end end end end end end -- ===================================================== -- INITIALIZATION -- ===================================================== world.addEventHandler(AFAC.EventHandler) -- Check for existing pilots function AFAC.checkForExistingPilots() for coalitionId = 1, 2 do local airGroups = coalition.getGroups(coalitionId, Group.Category.AIRPLANE) local heliGroups = coalition.getGroups(coalitionId, Group.Category.HELICOPTER) local allGroups = {} for _, group in ipairs(airGroups) do table.insert(allGroups, group) end for _, group in ipairs(heliGroups) do table.insert(allGroups, group) end for _, group in ipairs(allGroups) do local units = group:getUnits() if units then for _, unit in ipairs(units) do if unit and unit:isActive() and AFAC.isAFAC(unit) then local unitName = unit:getName() if unit:getPlayerName() and not AFAC.Data.pilots[unitName] then AFAC.addPilot(unit) end end end end end end end timer.scheduleFunction(AFAC.checkForExistingPilots, nil, timer.getTime() + 3) -- Periodic check for new pilots function AFAC.periodicCheck() AFAC.checkForExistingPilots() timer.scheduleFunction(AFAC.periodicCheck, nil, timer.getTime() + 10) end timer.scheduleFunction(AFAC.periodicCheck, nil, timer.getTime() + 15) AFAC.log("AFAC System initialized successfully") trigger.action.outText("Simple AFAC System v1.0 loaded successfully!", 10)