Version: v5.1
EWRS.lua is a DCS World Lua mission script that provides automated Early Warning Radar (EWR) picture reports and BRA (Bearing/Range/Altitude) callouts to players based on ground-based radar detection.
Designed for the 4YA Project Overlord WWII Normandy theatre, the script scans designated ground radar units, collects their detected targets, filters for airborne units, categorizes them into geographic zones, and periodically sends formatted reports to active players who have line-of-sight (LOS) to at least one friendly radar.
The system is designed to:
-- All tunables live at the top of EWRS.lua
ewrs.messageUpdateInterval = 240 -- seconds between full message cycles
ewrs.messageDisplayTime = 20 -- seconds each message stays on screen
ewrs.enableRedTeam = true -- enable for Axis/Red
ewrs.enableBlueTeam = true -- enable for Allies/Blue
ewrs.useImprovedDetectionLogic = true -- suppress range for targets without distance lock
ewrs.maxThreatDisplay = 2 -- max threats in BRA table (0 = unlimited)
ewrs.PictureCallouts = true -- enable zone picture reports
ewrs.NoOfPictures = 2 -- how many zone summaries to show
ewrs.heartbeatEnabled = false -- periodic "script is still running" in-game message for debugging
ewrs.heartbeatInterval = 10 -- seconds between heartbeat messages
ewrs.minAltFriendlyContact = 75 -- AGL below which player gets "blind" warning
ewrs.minAltEnemyContact = 10 -- AGL below which enemy contacts are ignored
ewrs.blueRadarsByName = {
"Allied RADAR ENG",
"Allied RADAR Beaches",
"RT2 Radars",
}
ewrs.redRadarsByName = {
"FREYA",
"Freya 1",
}
initializeZones() sets up the 23 named polygon zones covering the Normandy map and adjacent Channel areas.buildActivePlayers() queries net.get_player_list() for all connected players, resolves slots to actual units, and populates activePlayers.
radarIndex is reset when the player list is rebuilt to maintain stable round-robin messaging.pictureUpdate() drives the detection pipeline:
findRadarUnits() scans all group names in blueRadarsByName and redRadarsByName, populating blueEwrUnits and redEwrUnits.findDetectedTargets(side) iterates each radar unit, pulls getDetectedTargets(RADAR), applies an Earth curvature / horizon filter, and returns a deduplicated list.filterUnits() performs O(N) hash-map deduplication and filters to air units only (Object.Category.UNIT, descriptor categories 0-1).getDetectedTargets(RADAR) before iterating to avoid pairs(nil) errors.currentlyDetectedRedUnits and currentlyDetectedBlueUnits.picture() runs after radar detection:
mist.pointInPolygon() to determine which zone it falls into.sortPicture() copies zone data into redPicture / bluePicture and sorts by enemy contact count descending.23 zones cover: LE HAVRE, ROUEN, BERNAY, EVREUX, LAIGLE, ARGENTAN, ESSAY, CABOURG, VILLERS_BOCAGE, BAYEUX, FLERS, ST.LO, AVRANCHES, CARENTAN, CHARTRES, PARIS, BEAUVAIS, CHANNEL WEST, CHANNEL CENTRAL, CHANNEL EAST, ISLE OF WIGHT, SELSEY, BEACHY_HEAD.
buildThreatTable(activePlayer):
useImprovedDetectionLogic is enabled and the target has no distance lock, Range is set to N/A.minAltEnemyContact (10m AGL) are filtered out.ipairs iteration) and returned.displayMessageToAll() runs every N seconds (default 240s, up to 360s at high player counts):
groupSettings, builds the threat table.outText() formats and sends the BRA report via trigger.action.outTextForGroup.minAltFriendlyContact (75m), appends "Kenway/Brutus is blind".PictureCallouts is enabled, appends the top zone summaries.adjustInterval() runs every 10 minutes:
| Player Count | Interval |
|---|---|
| >= 40 | 360s |
| >= 30 | 300s |
| < 30 | 240s |
This keeps total message volume bounded as server population grows.
| Field | Purpose |
|---|---|
ewrs.blueEwrUnits / ewrs.redEwrUnits |
List of active radar unit names per side |
ewrs.currentlyDetectedRedUnits |
Detected targets visible to Blue radars |
ewrs.currentlyDetectedBlueUnits |
Detected targets visible to Red radars |
ewrs.activePlayers |
Table of connected players in valid slots |
ewrs.bluePicture / ewrs.redPicture |
Sorted zone summaries for picture reports |
ewrs.groupSettings |
Per-group message toggle state |
z corresponds to the mission editor y.ewrs.Zones are defined in the N1 Normandy coordinate space.| Function | Purpose |
|---|---|
ewrs.displayMessageToAll() |
Main loop. Picks one player per cycle, checks LOS, calls outText() or sends empty report. Schedules next run. |
ewrs.buildThreatTable(activePlayer) |
Returns a range-sorted table of {bearing, range, altitude, heading} for the player's nearest enemy contacts. |
ewrs.outText(activePlayer, threatTable, LOS) |
Formats and sends the BRA picture report (and optional zone summary) to the player's group via trigger.action.outTextForGroup. |
| Function | Purpose |
|---|---|
ewrs.getDetectedTargets() |
Wrapper that triggers detection for both sides. |
ewrs.findDetectedTargets(side) |
Iterates all radar units of side, pulls getDetectedTargets(RADAR), applies horizon/curvature filter, returns deduplicated list. |
ewrs.filterUnits(units) |
O(N) deduplication using a hash map. Also filters to Object.Category.UNIT and aircraft descriptor categories only (0-1). |
ewrs.findRadarUnits() |
Resolves radar group names into active unit lists (blueEwrUnits / redEwrUnits). |
| Function | Purpose |
|---|---|
ewrs.buildActivePlayers() |
Queries net API for all connected players, resolves slots to units, populates activePlayers. |
ewrs.addPlayer(playerName, groupID, unit) |
Adds one player record and bootstraps default groupSettings. |
ewrs.addGroupSettings(groupID) |
Initializes per-group settings (includes messages toggle). |
| Function | Purpose |
|---|---|
ewrs.picture() |
Resets zone counters, maps all detected targets into zones via mist.pointInPolygon, then calls sortPicture. |
ewrs.sortPicture(zonesData) |
Copies zone data into redPicture / bluePicture and sorts each by enemy contact count descending. |
| Function | Purpose |
|---|---|
ewrs.adjustInterval() |
Dynamic throttling: increases messageUpdateInterval at 30+ and 40+ player thresholds. |
ewrs.pictureUpdate() |
Master scheduler: runs every 15s, chains buildActivePlayers -> findRadarUnits -> picture. |
| Function | Purpose |
|---|---|
getCardinalDirection(deg) |
Converts 0-360 heading to 8-point compass string (N, NE, E, SE, S, SW, W, NW). |
getRangeLabel(range) |
Buckets range into "MAX / LONG / MEDIUM / NEAR / MERGED". |
getAltLabel(alt) |
Buckets altitude into "V HIGH / HIGH / MED-HI / MED-LO / LOW / V LOW". |
getDistance3D(...) |
Euclidean 3D distance. |
getBearing(x1, z1, x2, z2) |
Bearing from (x1,z1) to (x2,z2) in degrees. |
getHeadingFromVec(vec) |
Computes heading from velocity vector. |
filterUnits uses a hash map instead of nested loops.table.insert, math.sqrt, trigger.action.outTextForGroup, etc.) are aliased to locals for faster lookup. Added mathCos localization for consistency.mist.lua (MIST scripting framework — used for mist.pointInPolygon)| File | Purpose |
|---|---|
EWRS.lua |
Cleaned production script (use this one) |
EWRS_old.lua |
Original script (kept for reference) |
.luacheckrc |
Luacheck configuration for DCS globals whitelist |
README.md |
This documentation |
| Version | Date | Changes |
|---|---|---|
| v4.4 | Original | Base version by 4YA PO Modifications. Original zone-based picture reporting and BRA callout system. |
| v4.5 | -- | Replaced O(N^2) duplicate check in filterUnits with O(N) hash-map implementation. Replaced if/else heading logic with math-based lookup (getCardinalDirection). Inlined math functions (distance3D, bearing) in hot loops as local aliases. Localized global table lookups for performance. Cleaned up redundant code and comments. |
| v5.1 | Current | Lua 5.1 compliance fix: removed all goto/::continue:: statements (Lua 5.2 feature incompatible with DCS). Replaced with inverted guard conditions in buildThreatTable, buildActivePlayers, and filterUnits. Heartbeat message now sent to all players in-game (was: log only). Added script-load message to players + log ("EWRS Running v5.1", 5s display). Updated AGENTS.md with Lua 5.1 constraint. |
| v5.0 | -- | Dead code removal (unused constants, options, variables, mathAbs). Wired up PICTURE_DELAY and PICTURE_UPDATE_DELAY. Changed pairs to ipairs in buildThreatTable. Removed dead groupSettings fields (reference, measurements). Simplified greeting (removed dead "Reference: self" line). Added "No radar contacts found" message when LOS is false. Removed unused variables (side fetch in buildActivePlayers, contactType variable and unitType threat field). Spectator crash fix (slotID nil guard, unitName trimming, radarIndex reset). Nil guard for getDetectedTargets. Added mathCos localization. Added heartbeatEnabled / heartbeatInterval debug options. Added comprehensive README with Mermaid diagrams. |
mist.lua (MIST scripting framework) loaded in the mission before EWRSEWRS.lua in your mission's script folderEWRS.lua via "Do Script File"mist.lua is loaded first (add it before EWRS in the trigger order)Allied RADAR ENG, FREYA)ewrs.blueRadarsByName and ewrs.redRadarsByName in EWRS.lua to match your group namesewrs.Zones polygon coordinates to match your mapEWRS Running v5.1