[Q] Air intercept against Non hostile Bogeys
Moderator: MOD_Command
-
Knightpawn
- Posts: 781
- Joined: Mon Dec 02, 2024 12:28 pm
[Q] Air intercept against Non hostile Bogeys
Is the behavior of an intercepted non-hostile aircraft (eg. an unfriendly) affected as a result of the intercept? Will it turn around and go away (assuming that it is not fired upon) or will it continue to its mission / path?
- SunlitZelkova
- Posts: 426
- Joined: Tue Mar 06, 2018 11:49 pm
- Location: Portland, USA
Re: [Q] Air intercept against Non hostile Bogeys
I think it does not change. But making it do so with Lua sounds feasible. The aircraft would be ordered to RTB or switch to a different mission (example: have one CAP set up in the "no-fly zone" where interceptions will take place, and then a CAP mission set up in a "safe zone") once the intercepting aircraft or any aircraft from that side approach within a certain distance.
I was thinking this could be done with relative waypoints around the planes but then I realized they would disappear on landing and not exist on takeoff. I'm not proficient enough in Lua to know if using it to apply waypoints or setting up the "trigger radius" purely through the script would be easier.
I was thinking this could be done with relative waypoints around the planes but then I realized they would disappear on landing and not exist on takeoff. I'm not proficient enough in Lua to know if using it to apply waypoints or setting up the "trigger radius" purely through the script would be easier.
"One must not consider the individual objects without the whole."- Generalleutnant Gerhard von Scharnhorst, Royal Prussian Army
-
Knightpawn
- Posts: 781
- Joined: Mon Dec 02, 2024 12:28 pm
Re: [Q] Air intercept against Non hostile Bogeys
This is a Claude approach to the problem formulated by myself in 'Rule Implemented' for anyone amongst us with better knowledge to audit (problem formulation, code and whether the 15secs checks creates slowdowns).
updated 17 Aug 26
updated 17 Aug 26
Code: Select all
-- =====================================================================
-- INTERCEPTED AIRCRAFT -> RTB (MULTI-SIDE, SMOOTHED-LOAD VERSION)
-- Command: Modern Operations Lua script (Lua 5.3)
-- =====================================================================
-- PURPOSE:
-- Regulates the behaviour of COMPUTER-side aircraft when they are
-- intercepted by the player. SIDES_A below are the NON-PLAYER
-- (computer) sides. When one of their aircraft ('AC A') is
-- intercepted, it gives up and returns to base, and the player is
-- told the intercept succeeded.
--
-- RULE:
-- If an AC A is approached closer than TRIGGER_RANGE_NM (1 nm) by a
-- fighter or multirole aircraft of any side that is NOT (a) one of
-- the computer sides, (b) friendly, or (c) neutral to that computer
-- side (an interceptor - the 'Bogey' through the computer side's
-- eyes, i.e. normally the player), and the interceptor stays within
-- HOLD_RANGE_NM (5 nm) for HOLD_TIME_SECS (120 s), then:
-- * AC A is ordered to RTB, and
-- * MSG_INTERCEPT ("Bogey successfully intercepted") is shown to
-- the INTERCEPTOR'S side (the player).
-- RE-ARM: an AC A ordered RTB is exempt until it has recovered
-- (landed); the rule then applies afresh on its next sortie.
--
-- LOAD SMOOTHING (v3 - removes the periodic stutter at max compression):
-- Earlier versions did all work in one firing every ~60 s, causing a
-- visible pause. Work is now spread so NO single firing exceeds a
-- fixed API-call budget:
-- 1. ROSTER VIA unitsBy: aircraft lists come from one
-- side:unitsBy('Aircraft') call per side instead of a
-- GetUnit sweep over every unit of every side. (Falls back to
-- the full sweep if unitsBy is unavailable.)
-- 2. PERMANENT SUBTYPE CACHE: each interceptor-side aircraft is
-- classified fighter/non-fighter ONCE, ever; non-fighters are
-- never fetched again.
-- 3. BUDGETED SWEEP: the periodic position sweep fetches at most
-- WORK_BUDGET units per firing, continuing across consecutive
-- firings until complete, then schedules the next sweep from
-- worst-case closure speed (staleness compensated).
-- 4. AIRBORNE-ONLY SWEEPS: aircraft last seen on the ground are
-- re-checked only every GROUND_RECHECK_SECS, so steady-state
-- sweeps touch airborne aircraft only (parked fleets cost
-- nothing; a new launch is noticed within the recheck interval).
-- 5. HOT-PAIR FAST PATH: pairs that are armed or inside HOT_RANGE_NM
-- are re-checked every firing (a handful of fetches), so rule
-- timing accuracy is unchanged during actual engagements.
-- 6. Key store written only on state change (save/load persistence).
--
-- HOW TO INSTALL (Event Editor):
-- 1. Trigger: "Regular Time", interval = 15 seconds.
-- 2. Action: "Lua Script" containing this entire file.
-- 3. Event: repeatable, with the above trigger + action.
--
-- TUNING:
-- * WORK_BUDGET caps per-firing API load; lower it if any residual
-- stutter remains (sweeps just take more firings to complete).
-- * Reaction latency while quiet is up to MAX_SLEEP_SECS plus the
-- sweep duration; engaged timing accuracy = trigger interval.
-- * Uses ground truth (positions, postures), not sensor pictures.
-- =====================================================================
-- ------------------------- RULE CONFIGURATION ------------------------
local SIDES_A = { 'Angola', 'Russia' } -- <<< EDIT: the NON-PLAYER
-- (computer) sides regulated
-- by this script
local TRIGGER_RANGE_NM = 1.0 -- initial approach distance (nm)
local HOLD_RANGE_NM = 5.0 -- interceptor stays inside this (nm)
local HOLD_TIME_SECS = 120 -- ... for this long (seconds)
local MIN_AIRBORNE_KTS = 40 -- above this speed = airborne
local KEY_PREFIX = 'BOGEYRTB_' -- key-store prefix
local MSG_INTERCEPT = 'Bogey successfully intercepted'
-- Aircraft subtype codes counted as interceptors
-- (per Command Lua DataTypes: 2001 = Fighter, 2002 = Multirole)
local INTERCEPTOR_SUBTYPES = { ['2001'] = true, ['2002'] = true }
-- --------------------- PERFORMANCE CONFIGURATION ---------------------
local ROSTER_REFRESH_SECS = 120 -- re-query aircraft lists/postures
local HOT_RANGE_NM = 15 -- inside this: check every firing
local MAX_CLOSURE_KTS = 3000 -- worst-case combined closing speed
local MAX_SLEEP_SECS = 60 -- max gap between position sweeps
local MIN_SLEEP_SECS = 15 -- match the trigger interval
local WORK_BUDGET = 25 -- max unit fetches per firing for the
-- background sweep (the anti-stutter
-- knob: lower = smoother, slower)
local GROUND_RECHECK_SECS = 60 -- aircraft last seen on the ground
-- are re-checked only this often, so
-- sweeps effectively touch airborne
-- aircraft only (a new launch is
-- noticed within this interval)
-- ---------------------------------------------------------------------
local now = ScenEdit_CurrentTime()
-- Session-persistent state (survives between event firings within a
-- play session; rebuilt from the key store after a save/load).
if BOGEYRTB_STATE == nil then
BOGEYRTB_STATE = {
nextDue = 0, -- next scenario time a sweep is due
rosterAt = -1e12, -- last roster build time
acA = {}, -- { {guid=, side=}, ... } computer aircraft
acSide = {}, -- [guid] = side name (for messages)
intA = {}, -- { {guid=, watchers={side=true}}, ... }
-- interceptor-side aircraft (all types)
fighter = {}, -- [guid] = true/false, classified once ever
done = {}, -- [acGuid] = true (RTB'd this sortie)
armed = {}, -- [acGuid..'|'..intGuid] = armedAt time
hot = {}, -- [pid] = true: check this pair every firing
groundedAt = {}, -- [guid] = time last seen on the ground
sweep = nil, -- in-progress budgeted sweep, or nil
restored = false, -- key-store state re-read yet?
}
end
local S = BOGEYRTB_STATE
-- --------------------------- helpers ---------------------------------
local function isComputerSide(name)
for _, p in ipairs(SIDES_A) do
if name == p then return true end
end
return false
end
local function isAirborne(u)
return u.speed ~= nil and u.speed > MIN_AIRBORNE_KTS
end
-- Equirectangular approximation, result in nautical miles
local function approxRangeNM(lat1, lon1, lat2, lon2)
local dLat = lat2 - lat1
local dLon = lon2 - lon1
if dLon > 180 then dLon = dLon - 360
elseif dLon < -180 then dLon = dLon + 360 end
local mid = math.rad((lat1 + lat2) / 2)
local x = dLon * 60 * math.cos(mid)
local y = dLat * 60
return math.sqrt(x * x + y * y)
end
-- Core rule for one AC A / interceptor pair. Both wrappers airborne.
-- Returns rtbOrdered(bool), distance(nm).
local function processPair(acU, acSideName, intU, aLat, aLon, bLat, bLon)
local pid = acU.guid .. '|' .. intU.guid
local pairKey = KEY_PREFIX .. acU.guid .. '_' .. intU.guid
local d = approxRangeNM(aLat, aLon, bLat, bLon)
local armedAt = S.armed[pid]
if armedAt == nil then
if d < TRIGGER_RANGE_NM then
S.armed[pid] = now
S.hot[pid] = true
ScenEdit_SetKeyValue(pairKey, tostring(now))
print(string.format(
'BOGEYRTB: %s (%s) approached < %.1f nm by %s - timer started',
acU.name, acSideName, TRIGGER_RANGE_NM, intU.name))
elseif d <= HOT_RANGE_NM then
S.hot[pid] = true
else
S.hot[pid] = nil
end
else
if d > HOLD_RANGE_NM then
-- Interceptor broke off: reset the timer
S.armed[pid] = nil
ScenEdit_ClearKeyValue(pairKey)
if d > HOT_RANGE_NM then S.hot[pid] = nil end
elseif (now - armedAt) >= HOLD_TIME_SECS then
-- INTERCEPT SUCCESSFUL:
-- 1) computer aircraft gives up and goes home
ScenEdit_SetUnit({ guid = acU.guid, rtb = true })
S.done[acU.guid] = true
ScenEdit_SetKeyValue(KEY_PREFIX .. 'DONE_' .. acU.guid, '1')
S.armed[pid] = nil
S.hot[pid] = nil
ScenEdit_ClearKeyValue(pairKey)
-- 2) tell the INTERCEPTOR'S side (the player)
ScenEdit_SpecialMessage(intU.side,
MSG_INTERCEPT .. ' (' .. acU.name
.. ' is returning to base)')
print('BOGEYRTB: ' .. MSG_INTERCEPT .. ' - ' .. acU.name
.. ' (' .. acSideName .. ') ordered RTB; interceptor: '
.. intU.name)
return true, d
else
S.hot[pid] = true
end
end
return false, d
end
local function clearPair(pid, acGuid, intGuid)
S.hot[pid] = nil
if S.armed[pid] ~= nil then
S.armed[pid] = nil
ScenEdit_ClearKeyValue(KEY_PREFIX .. acGuid .. '_' .. intGuid)
end
end
-- ============== 1. HOT-PAIR FAST PATH (every firing) =================
if next(S.hot) ~= nil then
local cache = {}
local function fetch(g)
local c = cache[g]
if c == nil then
c = ScenEdit_GetUnit({ guid = g }) or false
cache[g] = c
end
if c == false then return nil end
return c
end
for pid in pairs(S.hot) do
local acG, inG = string.match(pid, '^(.+)|(.+)$')
if S.done[acG] then
S.hot[pid] = nil
else
local au = fetch(acG)
local iu = fetch(inG)
if au == nil or iu == nil
or not isAirborne(au) or not isAirborne(iu) then
clearPair(pid, acG, inG) -- dead, landed or grounded
else
processPair(au, S.acSide[acG] or au.side, iu,
tonumber(au.latitude), tonumber(au.longitude),
tonumber(iu.latitude), tonumber(iu.longitude))
end
end
end
end
-- ================ 2. START A SWEEP WHEN DUE ==========================
if S.sweep == nil and now >= S.nextDue then
-- Roster refresh (cheap: one unitsBy call per side)
if (now - S.rosterAt) >= ROSTER_REFRESH_SECS then
S.rosterAt = now
S.acA, S.acSide = {}, {}
for _, p in ipairs(SIDES_A) do
local side = VP_GetSide({ side = p })
if side == nil then
print('BOGEYRTB: side "' .. p .. '" not found - check SIDES_A')
else
local list = side:unitsBy('Aircraft')
if list ~= nil then
for i = 1, #list do
table.insert(S.acA, { guid = list[i].guid, side = p })
S.acSide[list[i].guid] = p
end
else -- fallback: full unit sweep (one-off cost)
for i = 1, #side.units do
local u = ScenEdit_GetUnit({ guid = side.units[i].guid })
if u ~= nil and u.type == 'Aircraft' then
table.insert(S.acA, { guid = u.guid, side = p })
S.acSide[u.guid] = p
end
end
end
end
end
S.intA = {}
for _, sd in ipairs(VP_GetSides()) do
if not isComputerSide(sd.name) then
local watchers, any = {}, false
for _, p in ipairs(SIDES_A) do
local posture = ScenEdit_GetSidePosture(p, sd.name)
if posture ~= 'F' and posture ~= 'N' then
watchers[p] = true
any = true
end
end
if any then
local side = VP_GetSide({ side = sd.name })
if side ~= nil then
local list = side:unitsBy('Aircraft')
if list ~= nil then
for i = 1, #list do
table.insert(S.intA,
{ guid = list[i].guid, watchers = watchers })
end
else -- fallback
for i = 1, #side.units do
local u = ScenEdit_GetUnit({ guid = side.units[i].guid })
if u ~= nil and u.type == 'Aircraft' then
table.insert(S.intA,
{ guid = u.guid, watchers = watchers })
end
end
end
end
end
end
end
-- One-time restore of persisted state (session start / load)
if not S.restored then
S.restored = true
for _, a in ipairs(S.acA) do
if ScenEdit_GetKeyValue(KEY_PREFIX .. 'DONE_' .. a.guid) ~= '' then
S.done[a.guid] = true
end
for _, b in ipairs(S.intA) do
local pv = ScenEdit_GetKeyValue(
KEY_PREFIX .. a.guid .. '_' .. b.guid)
if pv ~= '' then
local pid = a.guid .. '|' .. b.guid
S.armed[pid] = tonumber(pv)
S.hot[pid] = true
end
end
end
end
end
S.sweep = { startT = now, ai = 1, ii = 1, acPos = {}, intPos = {} }
end
-- ============ 3. BUDGETED SWEEP CHUNK (max WORK_BUDGET) ==============
if S.sweep ~= nil then
local sw = S.sweep
local budget = WORK_BUDGET
while budget > 0 and sw.ai <= #S.acA do
local a = S.acA[sw.ai]
sw.ai = sw.ai + 1
-- Aircraft recently seen on the ground: skip without fetching
local gAt = S.groundedAt[a.guid]
if gAt == nil or (now - gAt) >= GROUND_RECHECK_SECS then
budget = budget - 1
local u = ScenEdit_GetUnit({ guid = a.guid })
if u ~= nil then
if isAirborne(u) then
S.groundedAt[a.guid] = nil
table.insert(sw.acPos, { u = u, side = a.side,
lat = tonumber(u.latitude),
lon = tonumber(u.longitude) })
else
S.groundedAt[a.guid] = now
-- RE-ARM RULE: landed -> clear DONE flag + timers
if S.done[u.guid] then
S.done[u.guid] = nil
ScenEdit_ClearKeyValue(KEY_PREFIX .. 'DONE_' .. u.guid)
print('BOGEYRTB: ' .. u.name
.. ' has recovered - rule re-armed')
end
local pref = u.guid .. '|'
for pid in pairs(S.armed) do
if string.sub(pid, 1, #pref) == pref then
local intGuid = string.sub(pid, #pref + 1)
clearPair(pid, u.guid, intGuid)
end
end
end
end
end
end
while budget > 0 and sw.ii <= #S.intA do
local b = S.intA[sw.ii]
sw.ii = sw.ii + 1
local gAt = S.groundedAt[b.guid]
if S.fighter[b.guid] ~= false -- skip known non-fighters
and (gAt == nil or (now - gAt) >= GROUND_RECHECK_SECS) then
budget = budget - 1
local u = ScenEdit_GetUnit({ guid = b.guid })
if u ~= nil then
if S.fighter[b.guid] == nil then -- classify once, ever
S.fighter[b.guid] =
(INTERCEPTOR_SUBTYPES[tostring(u.subtype)] == true)
end
if S.fighter[b.guid] then
if isAirborne(u) then
S.groundedAt[b.guid] = nil
table.insert(sw.intPos,
{ u = u, watchers = b.watchers,
lat = tonumber(u.latitude),
lon = tonumber(u.longitude) })
else
S.groundedAt[b.guid] = now
end
end
end
end
end
-- ================ 4. SWEEP COMPLETE: pair logic ==================
if sw.ai > #S.acA and sw.ii > #S.intA then
S.sweep = nil
local minGap = nil
-- acPos/intPos contain airborne aircraft only (grounded ones
-- were filtered - and handled - at fetch time above)
for _, a in ipairs(sw.acPos) do
local ac = a.u
if not S.done[ac.guid] then
for _, b in ipairs(sw.intPos) do
if b.watchers[a.side] then
local rtbd, d = processPair(
ac, a.side, b.u, a.lat, a.lon, b.lat, b.lon)
if minGap == nil or d < minGap then minGap = d end
if rtbd then break end
end
end
end
end
-- ================ 5. SCHEDULE NEXT SWEEP =====================
local sleep
if next(S.hot) ~= nil then
sleep = MIN_SLEEP_SECS -- engaged: sweep continuously
elseif minGap == nil then
sleep = MAX_SLEEP_SECS -- no airborne pairs at all
else
-- Nothing can reach HOT_RANGE before the gap closes at
-- worst-case speed; compensate for sweep duration.
local closure = MAX_CLOSURE_KTS / 3600
local safe = (minGap - HOT_RANGE_NM) / closure
- (now - sw.startT)
sleep = math.max(MIN_SLEEP_SECS,
math.min(MAX_SLEEP_SECS, safe))
end
S.nextDue = now + sleep
end
end