EWRS - Early Warning Radar System

Version: v5.1

Overview

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:


How It Works

1. Script Initialisation

-- 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

2. Player Discovery

buildActivePlayers() queries net.get_player_list() for all connected players, resolves slots to actual units, and populates activePlayers.

3. Radar Detection (15-second cycle)

pictureUpdate() drives the detection pipeline:

  1. findRadarUnits() scans all group names in blueRadarsByName and redRadarsByName, populating blueEwrUnits and redEwrUnits.
  2. findDetectedTargets(side) iterates each radar unit, pulls getDetectedTargets(RADAR), applies an Earth curvature / horizon filter, and returns a deduplicated list.
  3. filterUnits() performs O(N) hash-map deduplication and filters to air units only (Object.Category.UNIT, descriptor categories 0-1).
  4. A nil guard checks getDetectedTargets(RADAR) before iterating to avoid pairs(nil) errors.
  5. Results are stored in currentlyDetectedRedUnits and currentlyDetectedBlueUnits.

4. Zone Aggregation (Picture Report)

picture() runs after radar detection:

  1. Resets all 23 zone counters to 0.
  2. For each detected target, calls mist.pointInPolygon() to determine which zone it falls into.
  3. Increments the appropriate zone's contact counter.
  4. 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.

5. Threat Table Building (per player)

buildThreatTable(activePlayer):

  1. Gets the player's enemy detected units based on coalition.
  2. For each target, calculates:
  3. If useImprovedDetectionLogic is enabled and the target has no distance lock, Range is set to N/A.
  4. Targets below minAltEnemyContact (10m AGL) are filtered out.
  5. Results are sorted by range ascending (ipairs iteration) and returned.

6. Message Display (round-robin)

displayMessageToAll() runs every N seconds (default 240s, up to 360s at high player counts):

  1. Picks 1 player round-robin from the active player list.
  2. Checks line-of-sight (LOS) to any friendly radar.
  3. If messages are enabled in groupSettings, builds the threat table.
  4. outText() formats and sends the BRA report via trigger.action.outTextForGroup.
  5. If player AGL is below minAltFriendlyContact (75m), appends "Kenway/Brutus is blind".
  6. If PictureCallouts is enabled, appends the top zone summaries.

7. Dynamic Interval Adjustment

adjustInterval() runs every 10 minutes:

Player Count Interval
>= 40 360s
>= 30 300s
< 30 240s

This keeps total message volume bounded as server population grows.

Key Data Stores

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

Coordinate System Notes


Logic Flow Diagrams

Overall System Architecture

flowchart TD A[Game Starts] --> B[Initialize Tables] B --> C[Schedule pictureUpdate Loop] C --> D[Schedule displayMessageToAll Loop] D --> E[Schedule adjustInterval Loop] E --> F{Every 15s} G[pictureUpdate] F --> H{Every N seconds} I[displayMessageToAll] F --> J{Every 10 min} K[adjustInterval] G --> F I --> H K --> J style A fill:#1a1a2e,color:#fff

pictureUpdate Loop (15s cycle)

flowchart TD A[pictureUpdate] --> B[buildActivePlayers] B --> C[Schedule findRadarUnits in 2s] C --> D[Schedule picture in 8s] D --> E[Schedule next pictureUpdate in 15s] style A fill:#1a1a2e,color:#fff

Radar Detection Pipeline

flowchart TD A[findRadarUnits] --> B[Scan blueRadarsByName] A --> C[Scan redRadarsByName] B --> D[Populate blueEwrUnits] C --> E[Populate redEwrUnits] D --> F[getDetectedTargets] E --> F F --> G[findDetectedTargets red] F --> H[findDetectedTargets blue] G --> I{For each radar unit} H --> I I --> J[getController:getDetectedTargets RADAR] J --> K{Target visible?} K -->|Yes| L[Earth curvature check] L -->|Above horizon| M[Collect target] M --> N[filterUnits] N --> O[Hash-map deduplication] O --> P[Filter air units only] P --> Q[Store currentlyDetectedXUnits] style A fill:#1a1a2e,color:#fff style K fill:#e94560,color:#fff style L fill:#e94560,color:#fff

Threat Table Building (per player)

flowchart TD A[buildThreatTable activePlayer] --> B[Get enemy detected units] B --> C[Get self position] C --> D[For each detected target] D --> E{Target exists?} E -->|No| D E -->|Yes| F[Calc bearing to target] F --> G[Calc heading from velocity] C --> I[Calc 3D range in km] G --> I I --> J{useImprovedDetectionLogic?} J -->|Yes & no distance| K[Range = N/A] J -->|No| L[Range = calculated] K --> M[Calc target AGL] L --> M M --> N{Alt > minAltEnemyContact?} N -->|Yes| O[Add to threatTable] N -->|No| D O --> P[Sort by range ascending] P --> Q[Return threatTable] style A fill:#1a1a2e,color:#fff style E fill:#e94560,color:#fff style J fill:#e94560,color:#fff style N fill:#e94560,color:#fff

Message Display Pipeline (per player)

flowchart TD A[displayMessageToAll] --> B{Players connected?} B -->|No| Z[Reschedule with default interval] B -->|Yes| C[Pick 1 player round-robin] C --> D[Check LOS to any friendly radar] D --> E{LOS true?} E -->|No| F["No radar contacts found"] E -->|Yes| G{Messages enabled?} G -->|No| H[Skip] G -->|Yes| I[buildThreatTable] I --> J[outText] J --> K{Player AGL < 75m?} K -->|Yes| L["Kenway/Brutus is blind"] K -->|No| M{Threats found?} M -->|Yes| N[Format BRA header] N --> O[For each threat up to max] O --> P[Format BRG/RNG/ALT/HDG row] P --> Q{PictureCallouts?} M -->|No| R[No nearby targets] R --> Q L --> S[Send message to group] Q -->|Yes| T[Append zone summaries] T --> S Q -->|No| S S --> U[Schedule next cycle / playerCount] style A fill:#1a1a2e,color:#fff style B fill:#e94560,color:#fff style E fill:#e94560,color:#fff style G fill:#e94560,color:#fff style K fill:#e94560,color:#fff style M fill:#e94560,color:#fff style Q fill:#e94560,color:#fff

Zone Processing (Picture Report)

flowchart TD A[picture] --> B[Reset all zone counters to 0] B --> C[processTargets redTargets / blueContacts] B --> D[processTargets blueTargets / redContacts] C --> E[For each target] D --> E E --> F[mist.pointInPolygon] F --> G{Inside zone?} G -->|Yes| H[Increment zone.contactKey] H --> I[Break to next target] G -->|No| J[Check next zone] J --> G I --> K[sortPicture] K --> L[Sort redPicture by blueContacts desc] K --> M[Sort bluePicture by redContacts desc] style A fill:#1a1a2e,color:#fff style G fill:#e94560,color:#fff

Player Management

flowchart TD A[buildActivePlayers] --> B[Query net.get_player_list] B --> C[For each player ID] C --> D[Get name, slot, unitName] D --> E[Unit.getByName] E --> F{Unit active?} F -->|No| C F -->|Yes| G{Coalition enabled?} G -->|Yes| H[addPlayer] G -->|No| C H --> I[Store player, groupID, unitname, side] I --> J{groupSettings missing?} J -->|Yes| K[addGroupSettings] J -->|No| L[Done] K --> L style A fill:#1a1a2e,color:#fff style F fill:#e94560,color:#fff style G fill:#e94560,color:#fff style J fill:#e94560,color:#fff

Interval Adjustment Logic

flowchart TD A[adjustInterval] --> B[Count activePlayers] B --> C{>= 40 players?} C -->|Yes| D[Set interval to 360s] C -->|No| E{>= 30 players?} E -->|Yes| F[Set interval to 300s] E -->|No| G[Set interval to 240s] D --> H[Reschedule adjustInterval in 600s] F --> H G --> H style A fill:#1a1a2e,color:#fff style C fill:#e94560,color:#fff style E fill:#e94560,color:#fff

Function Reference

Core / Display

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.

Detection & Filtering

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).

Player Management

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).

Picture / Zone Processing

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.

Timing & Intervals

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.

Helpers

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.

Performance Notes


Dependencies


Files

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 History

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.

Installation

  1. Prerequisites
  2. Mission Setup
  3. Configure Radar Units
  4. Zone Verification (Optional)
  5. Verification