mirror of
https://github.com/FlightControl-Master/MOOSE.git
synced 2026-08-16 07:56:35 +00:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 049aca91aa | |||
| a46bbcc2d2 | |||
| 7026a94301 | |||
| 92bc0389d3 | |||
| 7a5f6ad3c6 | |||
| 2a73d8d34a | |||
| dfe4db25ae | |||
| f66c77b027 | |||
| de3dc95c92 | |||
| d9a6f5daaf | |||
| 27fa920a8f | |||
| cf65cbf8fc | |||
| 2f229c1d8c | |||
| 7e07f2b4ea | |||
| d214d115c6 | |||
| fc1059c5d6 | |||
| 6e09ea7089 | |||
| 7587119843 | |||
| a5880b89c1 | |||
| 1be2c92a5c | |||
| 4c8a17fe57 | |||
| 64dea17982 | |||
| b1f86c702d | |||
| 1a9540d0d4 | |||
| cdfaac527c | |||
| 4e3bf9969b | |||
| 74c16bd4f0 | |||
| 55e048b322 | |||
| b31fb7ece3 | |||
| 149e3421d9 | |||
| 338ec34604 | |||
| 3d79a16d3a | |||
| efa08c1280 | |||
| 589e65963f | |||
| af4f20af67 | |||
| b1b8630d21 | |||
| 98ed7d50c1 | |||
| 4ba81cb8bc | |||
| 626b12f486 | |||
| e8f5214e0b | |||
| e4c7e809cb | |||
| aab16dd966 | |||
| 8b356f0913 | |||
| c3086b0265 | |||
| 599a2ba6fb |
@@ -735,6 +735,12 @@ do -- Event Handling
|
||||
-- @function [parent=#BASE] OnEventPlayerEnterAircraft
|
||||
-- @param #BASE self
|
||||
-- @param Core.Event#EVENTDATA EventData The EventData structure.
|
||||
|
||||
--- Occurs when an option is changed for an AI group.
|
||||
-- The event identifies the initiating group, but does not provide the option ID or value.
|
||||
-- @function [parent=#BASE] OnEventGroupChangeOption
|
||||
-- @param #BASE self
|
||||
-- @param Core.Event#EVENTDATA EventData The EventData structure.
|
||||
|
||||
--- Occurs when a player creates a dynamic cargo object from the F8 ground crew menu.
|
||||
-- *** NOTE *** this is a workarounf for DCS not creating these events as of Aug 2024.
|
||||
@@ -880,6 +886,22 @@ end
|
||||
|
||||
world.onEvent(Event)
|
||||
end
|
||||
|
||||
--- Creation of a `S_EVENT_GROUP_CHANGE_OPTION` event.
|
||||
-- @param #BASE self
|
||||
-- @param DCS#Time EventTime The time stamp of the event.
|
||||
-- @param DCS#Group Initiator The initiating group of the event.
|
||||
function BASE:CreateEventGroupChangeOption( EventTime, Initiator )
|
||||
self:F( { EventTime, Initiator } )
|
||||
|
||||
local Event = {
|
||||
id = EVENTS.GroupChangeOption,
|
||||
time = EventTime,
|
||||
initiator = Initiator,
|
||||
}
|
||||
|
||||
world.onEvent( Event )
|
||||
end
|
||||
|
||||
--- Creation of a S_EVENT_NEW_DYNAMIC_CARGO event.
|
||||
-- @param #BASE self
|
||||
|
||||
@@ -92,6 +92,8 @@ DATABASE = {
|
||||
ZONES_GOAL = {},
|
||||
WAREHOUSES = {},
|
||||
FLIGHTGROUPS = {},
|
||||
COHORTS={},
|
||||
LEGIONS={},
|
||||
FLIGHTCONTROLS = {},
|
||||
OPSZONES = {},
|
||||
PATHLINES = {},
|
||||
@@ -2087,6 +2089,40 @@ function DATABASE:FindOpsGroupFromUnit(unitname)
|
||||
end
|
||||
end
|
||||
|
||||
--
|
||||
|
||||
--- Add an OPS COHORT (SQUADRON, PLATOON, FLOTILLA) to the data base.
|
||||
-- @param #DATABASE self
|
||||
-- @param Ops.Cohort#COHORT cohort The cohort added to the DB.
|
||||
function DATABASE:AddCohort(cohort)
|
||||
self.COHORTS[cohort.name]=cohort
|
||||
end
|
||||
|
||||
--- Find an OPS COHORT (SQUADRON, PLATOON, FLOTILLA) in the data base.
|
||||
-- @param #DATABASE self
|
||||
-- @param #string cohortname Name of the cohort.
|
||||
-- @return Ops.Cohort#COHORT Cohort object.
|
||||
function DATABASE:FindCohort(cohortname)
|
||||
return self.COHORTS[cohortname]
|
||||
end
|
||||
|
||||
--- Add an OPS LEGION (AIRWING, BRIGADE, FLEET) to the data base.
|
||||
-- @param #DATABASE self
|
||||
-- @param Ops.Legion#LEGION legion The legion added to the DB.
|
||||
function DATABASE:AddLegion(legion)
|
||||
self.LEGIONS[legion.alias]=legion
|
||||
end
|
||||
|
||||
--- Find an OPS LEGION (AIRWING, BRIGADE, FLEET) in the data base.
|
||||
-- @param #DATABASE self
|
||||
-- @param #string legionname Name of the legion.
|
||||
-- @return Ops.Legion#LEGION Legion object.
|
||||
function DATABASE:FindLegion(legionname)
|
||||
return self.LEGIONS[legionname]
|
||||
end
|
||||
|
||||
--
|
||||
|
||||
--- Add a flight control to the data base.
|
||||
-- @param #DATABASE self
|
||||
-- @param OPS.FlightControl#FLIGHTCONTROL flightcontrol
|
||||
|
||||
@@ -278,6 +278,7 @@ EVENTS = {
|
||||
SimulationUnfreeze = world.event.S_EVENT_SIMULATION_UNFREEZE or -1,
|
||||
HumanAircraftRepairStart = world.event.S_EVENT_HUMAN_AIRCRAFT_REPAIR_START or -1,
|
||||
HumanAircraftRepairFinish = world.event.S_EVENT_HUMAN_AIRCRAFT_REPAIR_FINISH or -1,
|
||||
GroupChangeOption = world.event.S_EVENT_GROUP_CHANGE_OPTION or -1,
|
||||
-- dynamic cargo
|
||||
NewDynamicCargo = world.event.S_EVENT_NEW_DYNAMIC_CARGO or -1,
|
||||
DynamicCargoLoaded = world.event.S_EVENT_DYNAMIC_CARGO_LOADED or -1,
|
||||
@@ -291,23 +292,25 @@ EVENTS = {
|
||||
--
|
||||
-- * A (Object.Category.)UNIT : A UNIT object type is involved in the Event.
|
||||
-- * A (Object.Category.)STATIC : A STATIC object type is involved in the Event.
|
||||
-- * A GROUP : A DCS Group object is involved in the Event.
|
||||
--
|
||||
-- @type EVENTDATA
|
||||
-- @field #number id The identifier of the event.
|
||||
--
|
||||
-- @field DCS#Unit initiator (UNIT/STATIC/SCENERY) The initiating @{DCS#Unit} or @{DCS#StaticObject}.
|
||||
-- @field DCS#Unit initiator (UNIT/STATIC/SCENERY/GROUP) The initiating @{DCS#Unit} or @{DCS#StaticObject}. For `S_EVENT_GROUP_CHANGE_OPTION`, this is a DCS Group.
|
||||
-- @field DCS#Object.Category IniObjectCategory (UNIT/STATIC/SCENERY) The initiator object category ( Object.Category.UNIT or Object.Category.STATIC ).
|
||||
-- @field DCS#Unit IniDCSUnit (UNIT/STATIC) The initiating @{DCS#Unit} or @{DCS#StaticObject}.
|
||||
-- @field #string IniDCSUnitName (UNIT/STATIC) The initiating Unit name.
|
||||
-- @field Wrapper.Unit#UNIT IniUnit (UNIT/STATIC) The initiating MOOSE wrapper @{Wrapper.Unit#UNIT} of the initiator Unit object.
|
||||
-- @field #string IniUnitName (UNIT/STATIC) The initiating UNIT name (same as IniDCSUnitName).
|
||||
-- @field DCS#Group IniDCSGroup (UNIT) The initiating {DCSGroup#Group}.
|
||||
-- @field #string IniDCSGroupName (UNIT) The initiating Group name.
|
||||
-- @field Wrapper.Group#GROUP IniGroup (UNIT) The initiating MOOSE wrapper @{Wrapper.Group#GROUP} of the initiator Group object.
|
||||
-- @field #string IniGroupName UNIT) The initiating GROUP name (same as IniDCSGroupName).
|
||||
-- @field DCS#Group IniDCSGroup (UNIT/GROUP) The initiating @{DCS#Group}.
|
||||
-- @field #string IniDCSGroupName (UNIT/GROUP) The initiating Group name.
|
||||
-- @field Wrapper.Group#GROUP IniGroup (UNIT/GROUP) The initiating MOOSE wrapper @{Wrapper.Group#GROUP} of the initiator Group object.
|
||||
-- @field #string IniGroupName (UNIT/GROUP) The initiating GROUP name (same as IniDCSGroupName).
|
||||
-- @field DCS#Group.Category IniGroupCategory (GROUP) The category of the initiating group.
|
||||
-- @field #string IniPlayerName (UNIT) The name of the initiating player in case the Unit is a client or player slot.
|
||||
-- @field #string IniPlayerUCID (UNIT) The UCID of the initiating player in case the Unit is a client or player slot and on a multi-player server.
|
||||
-- @field DCS#coalition.side IniCoalition (UNIT) The coalition of the initiator.
|
||||
-- @field DCS#coalition.side IniCoalition (UNIT/GROUP) The coalition of the initiator.
|
||||
-- @field DCS#Unit.Category IniCategory (UNIT) The category of the initiator.
|
||||
-- @field #string IniTypeName (UNIT) The type name of the initiator.
|
||||
--
|
||||
@@ -734,6 +737,12 @@ local _EVENTMETA = {
|
||||
Side = "I",
|
||||
Event = "OnEventHumanAircraftRepairFinish",
|
||||
Text = "S_EVENT_HUMAN_AIRCRAFT_REPAIR_FINISH"
|
||||
},
|
||||
[EVENTS.GroupChangeOption] = {
|
||||
Order = 1,
|
||||
Side = "I",
|
||||
Event = "OnEventGroupChangeOption",
|
||||
Text = "S_EVENT_GROUP_CHANGE_OPTION"
|
||||
},
|
||||
-- dynamic cargo
|
||||
[EVENTS.NewDynamicCargo] = {
|
||||
@@ -1234,7 +1243,16 @@ function EVENT:onEvent( Event )
|
||||
|
||||
if Event.initiator then
|
||||
|
||||
Event.IniObjectCategory = Object.getCategory(Event.initiator)
|
||||
if Event.id == EVENTS.GroupChangeOption then
|
||||
Event.IniDCSGroup = Event.initiator
|
||||
Event.IniDCSGroupName = Group.getName(Event.initiator)
|
||||
Event.IniGroupName = Event.IniDCSGroupName
|
||||
Event.IniGroup = GROUP:FindByName(Event.IniDCSGroupName)
|
||||
Event.IniCoalition = Group.getCoalition(Event.initiator)
|
||||
Event.IniGroupCategory = Group.getCategory(Event.initiator)
|
||||
else
|
||||
Event.IniObjectCategory = Object.getCategory(Event.initiator)
|
||||
end
|
||||
|
||||
if Event.IniObjectCategory == Object.Category.STATIC then
|
||||
---
|
||||
|
||||
@@ -87,7 +87,8 @@ do -- world
|
||||
-- @field S_EVENT_SIMULATION_UNFREEZE = 58
|
||||
-- @field S_EVENT_HUMAN_AIRCRAFT_REPAIR_START = 59
|
||||
-- @field S_EVENT_HUMAN_AIRCRAFT_REPAIR_FINISH = 60
|
||||
-- @field S_EVENT_MAX = 61
|
||||
-- @field S_EVENT_GROUP_CHANGE_OPTION = 61
|
||||
-- @field S_EVENT_MAX = 62
|
||||
|
||||
--- The birthplace enumerator is used to define where an aircraft or helicopter has spawned in association with birth events.
|
||||
-- @type world.BirthPlace
|
||||
@@ -614,7 +615,6 @@ do -- Object
|
||||
-- @field SCENERY
|
||||
-- @field CARGO
|
||||
|
||||
---
|
||||
-- @type Object.Desc
|
||||
-- @extends #Desc
|
||||
-- @field #number life initial life level
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,760 +0,0 @@
|
||||
--- **Functional** - Enhanced Warsaw Pact GCI Controller.
|
||||
--
|
||||
-- ## Main Features:
|
||||
--
|
||||
-- * Guide AI and human pilots in Warsaw Pact Style. GCI Kernel Functions.
|
||||
-- * Advanced Tactics for Groups.
|
||||
-- * Many additional events that the mission designer can hook into.
|
||||
--
|
||||
-- ===
|
||||
--
|
||||
-- ### Author: **Applevangelist**
|
||||
--
|
||||
-- ===
|
||||
-- @module Functional.REDGCI_KERNEL
|
||||
-- @image Func_RedGCI.png
|
||||
-- @version 1.0.0
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
--- REDGCI_KERNEL class
|
||||
-- @type REDGCI_KERNEL
|
||||
|
||||
|
||||
---
|
||||
-- # RedGCI — Soviet GCI Doctrine & Player Guide
|
||||
--
|
||||
-- ## Philosophy: Централизованное управление (Centralized Control)
|
||||
--
|
||||
-- The fundamental difference between Soviet and NATO GCI is **who makes the tactical decision**.
|
||||
--
|
||||
-- In NATO doctrine, the GCI controller provides situational awareness — bearing, range, altitude, aspect — and the pilot decides how to prosecute the intercept. The pilot is an autonomous tactician. GCI is an advisor.
|
||||
--
|
||||
-- In Soviet doctrine, the GCI controller **directs**. The pilot executes. The controller selects the intercept geometry, assigns the heading, manages the radar, calls weapons free, and coordinates multi-ship tactics. The pilot's job is to fly the numbers and shoot when told. This is not a flaw — it is the system working as designed. Soviet fighter pilots were trained to be precise executors of GCI instructions, not independent tacticians. The ground radar network (PVO) was the brain; the aircraft was the weapon.
|
||||
--
|
||||
-- RedGCI models this philosophy faithfully.
|
||||
--
|
||||
-- ---
|
||||
--
|
||||
-- ## What to Expect as a Player
|
||||
--
|
||||
-- ### You will not be asked what you want to do.
|
||||
--
|
||||
-- There are no "recommend a vector" calls, no "at your discretion" callouts. The controller tells you your heading, your altitude, and your task. Your acknowledgement is assumed.
|
||||
--
|
||||
-- ### The controller manages your radar.
|
||||
--
|
||||
-- You do not decide when to turn your radar on. The GCI will tell you when to switch on (`локатор` / `Radar on`). Before that call, you fly cold and silent. This preserves your emissions discipline and prevents the target from getting an early RWR spike.
|
||||
--
|
||||
-- ### Weapons free is a controlled event.
|
||||
--
|
||||
-- You do not engage until the controller clears you (`цель разрешена` / `WEAPONS FREE`). The controller determines when geometry, range, and aspect are favorable. Shooting early breaks the coordinated intercept and may compromise your wingman's attack.
|
||||
--
|
||||
-- ### Radio calls are short and military.
|
||||
--
|
||||
-- Soviet GCI brevity is terse by design. Expect calls like:
|
||||
--
|
||||
-- - `"Сокол, курс 170, высота 4500."` — vector, altitude
|
||||
-- - `"Сокол, цель, пара, истребитель. Локатор."` — picture call on commit: count, type, radar on
|
||||
-- - `"Сокол, захват. Дальность 20. Цель разрешена."` — lock confirmed, range, weapons free
|
||||
-- - `"Сокол, молодец. Домой."` — good kill, RTB
|
||||
--
|
||||
-- There are no "BOGEY DOPE" requests, no "BRAA" calls, no "DECLARE" queries. The controller has already done that work. You fly the vector.
|
||||
--
|
||||
-- ---
|
||||
--
|
||||
-- ## State Flow — What the GCI is Doing Behind the Scenes
|
||||
--
|
||||
-- RedGCI manages a state machine that progresses through six phases. Understanding these phases helps you anticipate what call is coming next.
|
||||
--
|
||||
-- ```
|
||||
-- VECTOR → COMMIT → RADAR_CONTACT → VISUAL → MERGE → (SPLASH / ABORT / RTB)
|
||||
-- ```
|
||||
--
|
||||
-- ### VECTOR
|
||||
-- The controller has a track. You are being vectored onto an intercept geometry. Your radar is off. The controller is solving a collision course and updating your heading every tick. Altitude calls reflect the intercept geometry — you may be sent below the target (classic Soviet shoot-up doctrine for radar-limited types) or level/above (MiG-29/Su-27 lookdown geometry). Expect heading updates every 10–15 seconds.
|
||||
--
|
||||
-- **What you should do:** Fly the heading. Don't deviate. Don't turn your radar on yet. Speed is expected at 900kph TAS (depending on airframe)
|
||||
--
|
||||
-- ### COMMIT
|
||||
-- Range has closed to approximately 30km. The controller calls the picture: count and type. Your radar comes on. You are now committed to the intercept — turning away is no longer the default option. The controller is building your radar geometry toward a lock.
|
||||
--
|
||||
-- **What you should do:** Activate your radar. Acquire the target. Do not fire yet.
|
||||
--
|
||||
-- ### RADAR_CONTACT
|
||||
-- You have radar lock (or the AI has achieved it). The controller confirms lock and calls range. If geometry and range are favorable, weapons free follows immediately. If not — for example if aspect angle is unfavorable for a stern conversion — the controller holds fire and waits for better geometry.
|
||||
--
|
||||
-- **What you should do:** Maintain lock. Track the target. Wait for the weapons free call.
|
||||
--
|
||||
-- ### VISUAL
|
||||
-- Range has closed to approximately 5km — visual conditions. Weapons free is automatic at this point. You are now in the merge envelope.
|
||||
--
|
||||
-- **What you should do:** Engage.
|
||||
--
|
||||
-- ### MERGE
|
||||
-- Inside 2km. The GCI transitions to merge control: bearing to target, overshoot calls, separation instructions, reattack vectors. At this range the controller cannot see fine-grained geometry — merge calls are based on relative bearing and closure.
|
||||
--
|
||||
-- **What you should do:** Fight. Listen for overshoot, separation, and reattack calls.
|
||||
--
|
||||
-- ### SPLASH / ABORT / RTB
|
||||
-- - `SPLASH` — kill confirmed, RTB
|
||||
-- - `ABORT (THREAT)` — your RWR is spiked or a threat geometry has developed; break off immediately on the given heading
|
||||
-- - `ABORT (BINGO)` — fuel state critical; break off and return
|
||||
--
|
||||
-- ---
|
||||
--
|
||||
-- ## Multi-Ship (2v2) Tactics (REDGCI2v2)
|
||||
--
|
||||
-- When two fighters are dispatched against a threat, the GCI selects a tactic automatically based on the tactical situation. The tactic is applied at COMMIT — until then, both fighters are vectored together toward the intercept midpoint.
|
||||
--
|
||||
-- | Tactic | Description |
|
||||
-- |--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
-- | **PINCER** | Classic bracket. F1 and F2 split left and right, attacking from opposite angles simultaneously. Forces the target to choose which threat to react to. |
|
||||
-- | **HIGH-LOW** | Vertical split. One fighter attacks from below (radar up, clean sky background), one from above. Degrades the target's ability to acquire both simultaneously. |
|
||||
-- | **STAGGER** | BVR timing offset. F1 fires first at long range, F2 follows 8–13km behind to engage a maneuvering or defending target. |
|
||||
-- | **TRAIL** | Close trail. F1 is the shooter, F2 is support — ready to engage if F1 overshoots or is defeated. |
|
||||
-- | **GIRAFFE** | *(Historical — Iraq/Iran War, Mirage F1 vs F-14A)* F1 attacks at normal altitude, binding the AWG-9 radar. F2 flies nap-of-earth |
|
||||
-- | | (300–600m AGL) using ground clutter to degrade radar detection, then pulls up and fires from close range. |
|
||||
--
|
||||
-- During a tactic split, you may receive a heading that seems unusual — a large lateral offset or an unexpected altitude change. **Trust the vector.** The controller is positioning you for the tactic geometry. The merge point will bring you back onto the target.
|
||||
--
|
||||
-- ---
|
||||
--
|
||||
-- ## Dispatcher & CAP Flow (REDGCI_DISPATCHER)
|
||||
--
|
||||
-- When using the dispatcher layer, the full operational flow is:
|
||||
--
|
||||
-- ```
|
||||
-- Spawn at homeplate
|
||||
-- → Taxi and takeoff (template-controlled)
|
||||
-- → Transit to CAP zone
|
||||
-- → Orbit in assigned zone (radar cold, weapons safe)
|
||||
-- ↓ INTEL detects threat cluster
|
||||
-- → "Attention, radar contact. Pair, fighter, 45 kilometers." (all CAP fighters)
|
||||
-- ↓ Dispatcher assigns pair
|
||||
-- → "101 102, intercept. Pair, fighter." (dispatched pair)
|
||||
-- → VECTOR → COMMIT → RADAR_CONTACT → VISUAL → MERGE → SPLASH
|
||||
-- ↓ Engagement complete
|
||||
-- → AI: RTB waypoint → land → despawn → respawn after delay
|
||||
-- → Human: "101, mission complete. RTB, refuel and rearm."
|
||||
-- ↓ After RespawnDelay
|
||||
-- → New AI pair spawns into same CAP zone
|
||||
-- ```
|
||||
--
|
||||
-- Human players are dispatched first when available. If a human and AI are both in the CAP pool, the human is always assigned to the next intercept. AI fills gaps. The dispatcher does not send a single fighter if a pair is available — pairing is always preferred.
|
||||
--
|
||||
-- ---
|
||||
--
|
||||
-- ## Key Differences from NATO GCI at a Glance
|
||||
--
|
||||
-- | | Soviet (RedGCI) | NATO |
|
||||
-- |--------------------|--------------------------------------|------------------------------------|
|
||||
-- | Tactical decision | Controller | Pilot |
|
||||
-- | Radar management | Controller-commanded | Pilot-initiated |
|
||||
-- | Weapons free | Controller-called | Pilot-discretion (after WF) |
|
||||
-- | Heading calls | Prescriptive | Advisory |
|
||||
-- | Brevity style | Terse, military, positional | Standardized (BRAA, DECLARE, etc.) |
|
||||
-- | Multi-ship tactics | Centrally planned, applied at COMMIT | Mutually briefed, pilot-executed |
|
||||
-- | Pilot autonomy | Low (by design) | High |
|
||||
--
|
||||
-- **The Soviet system is not inferior** — it is optimized for a different kind of pilot and a different operational context. Mass interception of large NATO strike packages over defended Soviet airspace demanded centralized, efficient, high-throughput GCI control. RedGCI brings that experience to DCS.
|
||||
--
|
||||
-- @field #REDGCI_KERNEL
|
||||
REDGCI_KERNEL = {}
|
||||
REDGCI_KERNEL.version = "1.0.0"
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
-- Konstanten (aus gci_types.h)
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
|
||||
REDGCI_KERNEL.C = {
|
||||
RANGE_VECTOR_START = 60000.0,
|
||||
RANGE_COMMIT = 30000.0,
|
||||
RANGE_RADAR_FLOOR = 20000.0,
|
||||
RANGE_VISUAL = 5000.0,
|
||||
RANGE_MERGE = 2000.0,
|
||||
WF_RANGE_MAX = 25000.0,
|
||||
ALT_OFFSET_LOOKDOWN = 0.0,
|
||||
ASPECT_NOTCH_MIN = 80.0,
|
||||
ASPECT_NOTCH_MAX = 100.0,
|
||||
ASPECT_REAR_ATTACK = 120.0,
|
||||
MAX_TTI = 600.0,
|
||||
TICK_INTERVAL = 15.0,
|
||||
FUEL_BINGO = 0.25,
|
||||
DELAY_MIN = 3.0,
|
||||
DELAY_MAX = 8.0,
|
||||
DELAY_MERGE_MIN = 2.0,
|
||||
DELAY_MERGE_MAX = 4.0,
|
||||
TACTIC_PINCER = 0,
|
||||
TACTIC_HIGH_LOW = 1,
|
||||
TACTIC_STAGGER = 2,
|
||||
TACTIC_TRAIL = 3,
|
||||
TACTIC_GIRAFFE = 4, -- Irak/Iran-Doktrin: F1=Decoy normal, F2=Nap-of-Earth im Groundclutter
|
||||
PURSUIT_COLLISION = 0,
|
||||
PURSUIT_LEAD = 1,
|
||||
PURSUIT_PURE = 2,
|
||||
PURSUIT_NO_SOLUTION = 3,
|
||||
}
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
-- Hilfsfunktionen
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
|
||||
function REDGCI_KERNEL.clamp(v, lo, hi)
|
||||
if v < lo then return lo end
|
||||
if v > hi then return hi end
|
||||
return v
|
||||
end
|
||||
|
||||
function REDGCI_KERNEL.vec2len(x, z)
|
||||
return math.sqrt(x*x + z*z)
|
||||
end
|
||||
|
||||
function REDGCI_KERNEL.bearing(dx, dz)
|
||||
local b = math.deg(math.atan2(dx, dz))
|
||||
return b < 0.0 and b + 360.0 or b
|
||||
end
|
||||
|
||||
function REDGCI_KERNEL.randDelay(lo, hi)
|
||||
return lo + UTILS.LCGRandom() * (hi - lo)
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
-- Aspect Angle (0=Nose-on, 90=Beam, 180=Tail)
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
|
||||
function REDGCI_KERNEL.aspectAngle(target, observer)
|
||||
local dx = observer.x - target.x
|
||||
local dz = observer.z - target.z
|
||||
local range = REDGCI_KERNEL.vec2len(dx, dz)
|
||||
if range < 1.0 then return 0.0 end
|
||||
local nx = dx / range
|
||||
local nz = dz / range
|
||||
local spd = (target.speed or 1.0) + 1e-6
|
||||
local tvx = (target.vx or 0.0) / spd
|
||||
local tvz = (target.vz or 0.0) / spd
|
||||
local dot = REDGCI_KERNEL.clamp(tvx*nx + tvz*nz, -1.0, 1.0)
|
||||
return math.deg(math.acos(dot))
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
-- Closure Rate (positiv = Annäherung)
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
|
||||
function REDGCI_KERNEL.closureRate(f, t)
|
||||
local dx = t.x - f.x
|
||||
local dz = t.z - f.z
|
||||
local range = REDGCI_KERNEL.vec2len(dx, dz)
|
||||
if range < 1.0 then return 0.0 end
|
||||
local nx = dx / range
|
||||
local nz = dz / range
|
||||
local dvx = (t.vx or 0.0) - (f.vx or 0.0)
|
||||
local dvz = (t.vz or 0.0) - (f.vz or 0.0)
|
||||
return -(dvx*nx + dvz*nz)
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
-- _solveCollision (intern, GCI-Koordinaten)
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
|
||||
function REDGCI_KERNEL._solveCollision(f, t)
|
||||
local C = REDGCI_KERNEL.C
|
||||
local dx = t.x - f.x
|
||||
local dz = t.z - f.z
|
||||
local vtx = t.vx or 0.0
|
||||
local vtz = t.vz or 0.0
|
||||
local vf = f.speed or 1.0
|
||||
local a = vtx*vtx + vtz*vtz - vf*vf
|
||||
local b = 2.0 * (dx*vtx + dz*vtz)
|
||||
local c = dx*dx + dz*dz
|
||||
local sol_t = -1.0
|
||||
if math.abs(a) < 1.0 then
|
||||
if math.abs(b) > 0.01 then
|
||||
sol_t = -c / b
|
||||
else
|
||||
return false
|
||||
end
|
||||
else
|
||||
local disc = b*b - 4.0*a*c
|
||||
if disc < 0.0 then return false end
|
||||
local sq = math.sqrt(disc)
|
||||
local t1 = (-b - sq) / (2.0 * a)
|
||||
local t2 = (-b + sq) / (2.0 * a)
|
||||
if t1 > 0.0 and t2 > 0.0 then sol_t = math.min(t1, t2)
|
||||
elseif t1 > 0.0 then sol_t = t1
|
||||
elseif t2 > 0.0 then sol_t = t2
|
||||
else return false
|
||||
end
|
||||
end
|
||||
if sol_t < 0.0 or sol_t > C.MAX_TTI then return false end
|
||||
local ip = {
|
||||
x = t.x + vtx * sol_t,
|
||||
z = t.z + vtz * sol_t,
|
||||
y = math.max(t.y + C.ALT_OFFSET_LOOKDOWN, 300.0),
|
||||
}
|
||||
local hdg = REDGCI_KERNEL.bearing(ip.x - f.x, ip.z - f.z)
|
||||
return true, hdg, sol_t, ip
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
-- _solveLead (intern, GCI-Koordinaten)
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
|
||||
function REDGCI_KERNEL._solveLead(f, t)
|
||||
local C = REDGCI_KERNEL.C
|
||||
local dx = t.x - f.x
|
||||
local dz = t.z - f.z
|
||||
local range = REDGCI_KERNEL.vec2len(dx, dz)
|
||||
if range < 1.0 then return 0.0, 0.0 end
|
||||
local base_bearing = REDGCI_KERNEL.bearing(dx, dz)
|
||||
local aspect_rad = math.rad(REDGCI_KERNEL.aspectAngle(t, f))
|
||||
local speed_ratio = (t.speed or 1.0) / ((f.speed or 1.0) + 1e-6)
|
||||
local sin_lead = REDGCI_KERNEL.clamp(speed_ratio * math.sin(aspect_rad), -1.0, 1.0)
|
||||
local lead_deg = math.deg(math.asin(sin_lead))
|
||||
local hdg
|
||||
if math.abs(lead_deg) < 45.0 then
|
||||
hdg = (base_bearing + lead_deg + 360.0) % 360.0
|
||||
else
|
||||
hdg = base_bearing
|
||||
end
|
||||
local closing = REDGCI_KERNEL.closureRate(f, t)
|
||||
if closing < 50.0 then closing = 50.0 end
|
||||
local tti = math.min(range / closing, C.MAX_TTI)
|
||||
return hdg, tti
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
-- computeIntercept (GCI-Koordinaten intern)
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
|
||||
function REDGCI_KERNEL.computeIntercept(fighter, target)
|
||||
local C = REDGCI_KERNEL.C
|
||||
local dx = target.x - fighter.x
|
||||
local dz = target.z - fighter.z
|
||||
local sol = {
|
||||
heading_deg = 0.0,
|
||||
time_to_intercept = C.MAX_TTI,
|
||||
intercept_point = { x=target.x, z=target.z, y=target.y },
|
||||
target_alt = target.y,
|
||||
solution_found = false,
|
||||
aspect_angle = REDGCI_KERNEL.aspectAngle(target, fighter),
|
||||
range = REDGCI_KERNEL.vec2len(dx, dz),
|
||||
mode = C.PURSUIT_NO_SOLUTION,
|
||||
weapons_free = false,
|
||||
}
|
||||
local closure = REDGCI_KERNEL.closureRate(fighter, target)
|
||||
local projected_range = sol.range - closure * C.TICK_INTERVAL
|
||||
sol.weapons_free = (sol.range < C.WF_RANGE_MAX) or
|
||||
(closure > 0.0 and projected_range < C.WF_RANGE_MAX)
|
||||
local ok, hdg, tti, ip = REDGCI_KERNEL._solveCollision(fighter, target)
|
||||
if ok then
|
||||
sol.heading_deg = hdg
|
||||
sol.time_to_intercept = tti
|
||||
sol.intercept_point = ip
|
||||
sol.solution_found = true
|
||||
sol.mode = C.PURSUIT_COLLISION
|
||||
return sol
|
||||
end
|
||||
local lhdg, ltti = REDGCI_KERNEL._solveLead(fighter, target)
|
||||
sol.heading_deg = lhdg
|
||||
sol.time_to_intercept = ltti
|
||||
sol.solution_found = true
|
||||
sol.mode = C.PURSUIT_LEAD
|
||||
sol.intercept_point = {
|
||||
x = target.x + (target.vx or 0.0) * ltti,
|
||||
z = target.z + (target.vz or 0.0) * ltti,
|
||||
y = math.max(target.y + C.ALT_OFFSET_LOOKDOWN, 300.0),
|
||||
}
|
||||
return sol
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
-- computeInterceptDCS (DCS-Koordinaten Ein/Aus)
|
||||
-- Flat-Rückgabe kompatibel mit alter C-API
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
|
||||
function REDGCI_KERNEL.computeInterceptDCS(f, t)
|
||||
local f_gci = { x=f.z, z=f.x, y=f.y, vx=f.vz, vz=f.vx, vy=f.vy, speed=f.spd }
|
||||
local t_gci = { x=t.z, z=t.x, y=t.y, vx=t.vz, vz=t.vx, vy=t.vy, speed=t.spd }
|
||||
local sol = REDGCI_KERNEL.computeIntercept(f_gci, t_gci)
|
||||
-- GCI→DCS: ip.z(Nord)→DCS.x, ip.x(Ost)→DCS.z
|
||||
return sol.heading_deg,
|
||||
sol.time_to_intercept,
|
||||
sol.mode,
|
||||
sol.weapons_free,
|
||||
sol.range,
|
||||
sol.aspect_angle,
|
||||
sol.intercept_point.z,
|
||||
sol.intercept_point.x,
|
||||
sol.intercept_point.y,
|
||||
sol.target_alt
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
-- computeSplit (GCI-Koordinaten intern)
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
|
||||
function REDGCI_KERNEL.computeSplit(f1, f2, tgt, tactic, variation)
|
||||
local C = REDGCI_KERNEL.C
|
||||
variation = REDGCI_KERNEL.clamp(variation or 0.5, 0.0, 1.0)
|
||||
tactic = tactic or C.TACTIC_PINCER
|
||||
local mid_x = (f1.x + f2.x) * 0.5
|
||||
local mid_z = (f1.z + f2.z) * 0.5
|
||||
local dx = tgt.x - mid_x
|
||||
local dz = tgt.z - mid_z
|
||||
local rng = REDGCI_KERNEL.vec2len(dx, dz)
|
||||
if rng < 1.0 then rng = 1.0 end
|
||||
local ax = dx / rng
|
||||
local az = dz / rng
|
||||
local px = -az
|
||||
local pz = ax
|
||||
|
||||
local plan = {
|
||||
tactic = tactic,
|
||||
wp_f1 = { x=0, z=0, y=0 },
|
||||
wp_f2 = { x=0, z=0, y=0 },
|
||||
merge_f1 = { x=0, z=0, y=0 },
|
||||
merge_f2 = { x=0, z=0, y=0 },
|
||||
}
|
||||
|
||||
local function clamp_alt(pt)
|
||||
if pt.y < 300.0 then pt.y = 300.0 end
|
||||
end
|
||||
|
||||
local function clamp_alt_noe(pt)
|
||||
if pt.y < 150.0 then pt.y = 150.0 end
|
||||
end
|
||||
|
||||
-- GIRAFFE F2 fliegt NOE — separater clamp mit niedrigerer Mindesthöhe
|
||||
local function clamp_alt_noe(pt)
|
||||
if pt.y < 150.0 then pt.y = 150.0 end
|
||||
end
|
||||
|
||||
if tactic == C.TACTIC_PINCER then
|
||||
local spread = 12000.0 + variation * 5000.0
|
||||
local approach = rng * 0.45
|
||||
local merge_off = spread * 0.30
|
||||
plan.wp_f1 = { x=mid_x+ax*approach+px*spread, z=mid_z+az*approach+pz*spread, y=tgt.y }
|
||||
plan.wp_f2 = { x=mid_x+ax*approach-px*spread, z=mid_z+az*approach-pz*spread, y=tgt.y }
|
||||
plan.merge_f1 = { x=tgt.x-ax*3000+px*merge_off, z=tgt.z-az*3000+pz*merge_off, y=tgt.y }
|
||||
plan.merge_f2 = { x=tgt.x-ax*3000-px*merge_off, z=tgt.z-az*3000-pz*merge_off, y=tgt.y }
|
||||
|
||||
elseif tactic == C.TACTIC_HIGH_LOW then
|
||||
local vert = 3000.0 + variation * 1500.0
|
||||
local approach = rng * 0.50
|
||||
local side_off = 2000.0
|
||||
plan.wp_f1 = { x=mid_x+ax*approach+px*side_off, z=mid_z+az*approach+pz*side_off, y=tgt.y-500.0 }
|
||||
plan.wp_f2 = { x=mid_x+ax*approach-px*side_off, z=mid_z+az*approach-pz*side_off, y=tgt.y+vert }
|
||||
plan.merge_f1 = { x=tgt.x-ax*3000+px*side_off, z=tgt.z-az*3000+pz*side_off, y=plan.wp_f1.y }
|
||||
plan.merge_f2 = { x=tgt.x-ax*3000-px*side_off, z=tgt.z-az*3000-pz*side_off, y=plan.wp_f2.y }
|
||||
|
||||
elseif tactic == C.TACTIC_STAGGER then
|
||||
local lag = 8000.0 + variation * 3000.0
|
||||
local lead_dist = rng * 0.85
|
||||
plan.wp_f1 = { x=mid_x+ax*lead_dist, z=mid_z+az*lead_dist, y=tgt.y }
|
||||
plan.wp_f2 = { x=mid_x+ax*(lead_dist-lag), z=mid_z+az*(lead_dist-lag), y=tgt.y }
|
||||
plan.merge_f1 = { x=plan.wp_f1.x, z=plan.wp_f1.z, y=plan.wp_f1.y }
|
||||
plan.merge_f2 = { x=plan.wp_f2.x, z=plan.wp_f2.z, y=plan.wp_f2.y }
|
||||
|
||||
elseif tactic == C.TACTIC_TRAIL then
|
||||
local lag = 3000.0 + variation * 2000.0
|
||||
local lead_dist = rng * 0.85
|
||||
local side_off = 500.0
|
||||
plan.wp_f1 = { x=mid_x+ax*lead_dist, z=mid_z+az*lead_dist, y=tgt.y }
|
||||
plan.wp_f2 = { x=mid_x+ax*(lead_dist-lag)+px*side_off, z=mid_z+az*(lead_dist-lag)+pz*side_off, y=tgt.y }
|
||||
plan.merge_f1 = { x=plan.wp_f1.x, z=plan.wp_f1.z, y=plan.wp_f1.y }
|
||||
plan.merge_f2 = { x=plan.wp_f2.x, z=plan.wp_f2.z, y=plan.wp_f2.y }
|
||||
|
||||
elseif tactic == C.TACTIC_GIRAFFE then
|
||||
-- ── GIRAFFE: Irak/Iran-Doktrin (Mirage F1 vs F-14A) ──────────────
|
||||
--
|
||||
-- F1 = Decoy: normale Höhe, direkter Anflug, bindet AWG-9 Radar.
|
||||
-- Zieht Phoenixe auf sich — gibt F2 Zeit für Pull-up.
|
||||
--
|
||||
-- F2 = Killer: Nap-of-Earth (~300-600m AGL/MSL) weit seitlich versetzt.
|
||||
-- Nutzt Groundclutter um AWG-9 Look-Down zu degradieren.
|
||||
-- Nähert sich bis 10km vor Ziel, dann Pull-up und Schuss.
|
||||
-- Seitlicher Versatz ~8-12km damit F2 nicht im Radarsektor
|
||||
-- des AWG-9 liegt während F1 den Lock hält.
|
||||
--
|
||||
-- Historisch: Iran-Irak Krieg 1982-88. Irakische Mirage F1EQ nutzten
|
||||
-- dieses Profil gegen F-14A/AIM-54 Phoenix — der AWG-9 hatte trotz
|
||||
-- Look-Down-Fähigkeit bei sehr tiefen Zielen über unebenen Terrain
|
||||
-- (Zagros-Berge, Khuzestan-Ebene) erhöhte Clutter-Probleme.
|
||||
--
|
||||
-- Höhen: neutral (0.0) — Lua addiert AltOffset + terrain-awareness.
|
||||
-- Mindesthöhe 300m MSL wird am Ende geclampt.
|
||||
|
||||
local noe_alt = 300.0 + variation * 300.0 -- 300-600m MSL (Nap-of-Earth)
|
||||
local side_off = 8000.0 + variation * 4000.0 -- 8-12km seitlich (aus AWG-9 Sektor)
|
||||
local approach = rng * 0.60 -- F1 direkter Anflug, 60% des Weges
|
||||
local noe_dist = rng * 0.75 -- F2 nähert sich weiter ran vor Pull-up
|
||||
|
||||
plan.wp_f1 = { x=mid_x+ax*approach+px*2000, z=mid_z+az*approach+pz*2000, y=tgt.y }
|
||||
plan.merge_f1 = { x=tgt.x-ax*5000+px*2000, z=tgt.z-az*5000+pz*2000, y=tgt.y }
|
||||
plan.wp_f2 = { x=mid_x+ax*noe_dist-px*side_off, z=mid_z+az*noe_dist-pz*side_off, y=noe_alt, absolute_alt=true }
|
||||
plan.merge_f2 = { x=tgt.x-ax*10000-px*side_off*0.3, z=tgt.z-az*10000-pz*side_off*0.3, y=tgt.y }
|
||||
|
||||
else
|
||||
plan.wp_f1 = { x=tgt.x, z=tgt.z, y=tgt.y }
|
||||
plan.wp_f2 = { x=tgt.x, z=tgt.z, y=tgt.y }
|
||||
plan.merge_f1 = { x=tgt.x, z=tgt.z, y=tgt.y }
|
||||
plan.merge_f2 = { x=tgt.x, z=tgt.z, y=tgt.y }
|
||||
end
|
||||
|
||||
clamp_alt(plan.wp_f1)
|
||||
if tactic == C.TACTIC_GIRAFFE then
|
||||
clamp_alt_noe(plan.wp_f2)
|
||||
else
|
||||
clamp_alt(plan.wp_f2)
|
||||
end
|
||||
clamp_alt(plan.merge_f1)
|
||||
clamp_alt(plan.merge_f2)
|
||||
return plan
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
-- computeSplitDCS (DCS-Koordinaten Ein/Aus)
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
|
||||
function REDGCI_KERNEL.computeSplitDCS(f1, f2, tgt, tactic, variation)
|
||||
local function dcs2gci(u)
|
||||
return { x=u.z, z=u.x, y=u.y, vx=u.vz or 0, vz=u.vx or 0, speed=u.spd or 0 }
|
||||
end
|
||||
local function gci2dcs(pt)
|
||||
return { x=pt.z, z=pt.x, y=pt.y, absolute_alt=pt.absolute_alt }
|
||||
end
|
||||
local tgt_gci = { x=tgt.z, z=tgt.x, y=tgt.y }
|
||||
local plan = REDGCI_KERNEL.computeSplit(
|
||||
dcs2gci(f1), dcs2gci(f2), tgt_gci, tactic, variation)
|
||||
plan.wp_f1 = gci2dcs(plan.wp_f1)
|
||||
plan.wp_f2 = gci2dcs(plan.wp_f2)
|
||||
plan.merge_f1 = gci2dcs(plan.merge_f1)
|
||||
plan.merge_f2 = gci2dcs(plan.merge_f2)
|
||||
return plan
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
-- buildTransmission (Port von gci_build_transmission)
|
||||
--
|
||||
-- ctx/prev Felder: state, prev_state, ticks_in_state, range,
|
||||
-- aspect_angle, closure_rate, altitude_delta, fuel_fraction
|
||||
-- sol Felder: heading_deg, time_to_intercept, target_alt,
|
||||
-- range, aspect_angle, weapons_free
|
||||
-- Rückgabe: { token_str, delay_sec, weapons_free, silence }
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
|
||||
function REDGCI_KERNEL.buildTransmission(ctx, prev, sol)
|
||||
local C = REDGCI_KERNEL.C
|
||||
local tx = {
|
||||
token_str = "",
|
||||
delay_sec = REDGCI_KERNEL.clamp(
|
||||
REDGCI_KERNEL.randDelay(C.DELAY_MIN, C.DELAY_MAX), 3.0, 8.0),
|
||||
weapons_free = sol.weapons_free,
|
||||
silence = false,
|
||||
priority = 50,
|
||||
}
|
||||
|
||||
local hdg_i = math.floor((sol.heading_deg or 0) + 0.5)
|
||||
local alt_i = math.floor((sol.target_alt or 0) / 100.0 + 0.5) * 100
|
||||
local rng_km = math.floor((ctx.range or 0) / 1000.0 + 0.5)
|
||||
local aspect_i = math.floor((ctx.aspect_angle or 0) + 0.5)
|
||||
local delay = tx.delay_sec
|
||||
local state = ctx.state or "VECTOR"
|
||||
local prev_state = prev.state or "VECTOR"
|
||||
local ticks = ctx.ticks_in_state or 0
|
||||
|
||||
local function emit(fmt, ...)
|
||||
tx.token_str = string.format(fmt, ...)
|
||||
end
|
||||
|
||||
if state == "VECTOR" then
|
||||
if prev_state ~= "VECTOR" then
|
||||
local tti_m = math.floor((sol.time_to_intercept or 0) / 60.0)
|
||||
local tti_s = math.floor(sol.time_to_intercept or 0) % 60
|
||||
if tti_m > 0 then
|
||||
emit("VECTOR_WITH_TTI|hdg=%d|alt=%d|rng=%d|tti_m=%d|tti_s=%d|delay=%.1f",
|
||||
hdg_i, alt_i, rng_km, tti_m, tti_s, delay)
|
||||
else
|
||||
emit("VECTOR|hdg=%d|alt=%d|rng=%d|delay=%.1f",
|
||||
hdg_i, alt_i, rng_km, delay)
|
||||
end
|
||||
else
|
||||
local hdg_delta = math.abs((sol.heading_deg or 0) - (ctx.aspect_angle or 0))
|
||||
if hdg_delta < 5.0 and ticks > 3 then
|
||||
tx.silence = true
|
||||
else
|
||||
emit("VECTOR|hdg=%d|alt=%d|rng=%d|delay=%.1f",
|
||||
hdg_i, alt_i, rng_km, delay)
|
||||
end
|
||||
end
|
||||
|
||||
elseif state == "COMMIT" then
|
||||
if prev_state ~= "COMMIT" then
|
||||
tx.priority = 100
|
||||
emit("COMMIT_FIRST|hdg=%d|alt=%d|rng=%d|aspect=%d|delay=%.1f",
|
||||
hdg_i, alt_i, rng_km, aspect_i, delay)
|
||||
elseif ticks == 6 then
|
||||
emit("COMMIT_NO_LOCK|hdg=%d|rng=%d|aspect=%d|delay=%.1f",
|
||||
hdg_i, rng_km, aspect_i, delay)
|
||||
elseif ticks > 8 then
|
||||
emit("COMMIT_NUDGE|hdg=%d|aspect=%d|delay=%.1f",
|
||||
hdg_i, aspect_i, delay)
|
||||
else
|
||||
tx.silence = true
|
||||
end
|
||||
|
||||
elseif state == "RADAR_CONTACT" then
|
||||
if prev_state ~= "RADAR_CONTACT" then
|
||||
if sol.weapons_free then
|
||||
tx.priority = 100
|
||||
emit("RADAR_LOCK_WF|rng=%d|delay=%.1f", rng_km, delay)
|
||||
tx.weapons_free = true
|
||||
else
|
||||
emit("RADAR_LOCK_HOLD|rng=%d|delay=%.1f", rng_km, delay)
|
||||
end
|
||||
elseif not sol.weapons_free
|
||||
and (ctx.range or 0) < C.WF_RANGE_MAX
|
||||
and (ctx.aspect_angle or 0) > C.ASPECT_REAR_ATTACK
|
||||
and prev_state == "RADAR_CONTACT" then
|
||||
emit("RADAR_WF_NOW|rng=%d|delay=%.1f", rng_km, delay)
|
||||
tx.priority = 100
|
||||
tx.weapons_free = true
|
||||
else
|
||||
tx.silence = true
|
||||
end
|
||||
|
||||
elseif state == "VISUAL" then
|
||||
if prev_state ~= "VISUAL" then
|
||||
emit("VISUAL_CONFIRM|rng=%d|delay=%.1f", rng_km, delay)
|
||||
tx.weapons_free = true
|
||||
else
|
||||
tx.silence = true
|
||||
end
|
||||
|
||||
elseif state == "NOTCH" then
|
||||
if prev_state ~= "NOTCH" then
|
||||
emit("NOTCH_ENTRY|delay=1.5")
|
||||
elseif ticks % 8 == 0 then
|
||||
emit("NOTCH_UPDATE|rng=%d|aspect=%d|delay=%.1f",
|
||||
rng_km, aspect_i, delay)
|
||||
else
|
||||
tx.silence = true
|
||||
end
|
||||
|
||||
elseif state == "ABORT" then
|
||||
tx.delay_sec = 1.5
|
||||
if (ctx.fuel_fraction or 1.0) < C.FUEL_BINGO then
|
||||
emit("ABORT_BINGO|hdg=%d|delay=1.5", hdg_i)
|
||||
else
|
||||
emit("ABORT_THREAT|hdg=%d|delay=1.5", hdg_i)
|
||||
end
|
||||
|
||||
else
|
||||
tx.silence = true
|
||||
end
|
||||
|
||||
return tx
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
-- mergeTransition (Port von gci_merge_transition)
|
||||
--
|
||||
-- ctx Felder: phase, ticks_in_phase, range, bearing_to_target,
|
||||
-- closure_rate, altitude_delta, pass_count, radar_lost
|
||||
-- Rückgabe: phase string
|
||||
-- "ENTRY"|"OVERSHOOT"|"SEPARATION"|"REATTACK"|"LOST"|"SPLASH"
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
|
||||
function REDGCI_KERNEL.mergeTransition(ctx, prev)
|
||||
if ctx.radar_lost then return "LOST" end
|
||||
if (ctx.closure_rate or 0) < -30.0 and (ctx.range or 0) > 3000.0 then
|
||||
return "SEPARATION"
|
||||
end
|
||||
local brg = ctx.bearing_to_target or 0
|
||||
if brg > 100.0 and brg < 260.0 and (ctx.range or 0) < 5000.0 then
|
||||
return "OVERSHOOT"
|
||||
end
|
||||
if ctx.phase == "SEPARATION" and (ctx.pass_count or 0) < 3 then
|
||||
return "REATTACK"
|
||||
end
|
||||
return ctx.phase
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
-- buildMergeTransmission (Port von gci_build_merge_transmission)
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
|
||||
function REDGCI_KERNEL.buildMergeTransmission(ctx, prev)
|
||||
local C = REDGCI_KERNEL.C
|
||||
local tx = {
|
||||
token_str = "",
|
||||
delay_sec = REDGCI_KERNEL.clamp(
|
||||
REDGCI_KERNEL.randDelay(C.DELAY_MERGE_MIN, C.DELAY_MERGE_MAX), 2.0, 5.0),
|
||||
weapons_free = false,
|
||||
silence = false,
|
||||
priority = 50,
|
||||
}
|
||||
|
||||
local brg = math.floor((ctx.bearing_to_target or 0) + 0.5)
|
||||
local rng_km = math.floor((ctx.range or 0) / 1000.0 + 0.5)
|
||||
local delay = tx.delay_sec
|
||||
local phase = ctx.phase or "ENTRY"
|
||||
local prev_phase = prev.phase or "ENTRY"
|
||||
local ticks = ctx.ticks_in_phase or 0
|
||||
|
||||
local dir_rl = ((ctx.bearing_to_target or 0) < 180.0) and "right" or "left"
|
||||
local alt_d = ctx.altitude_delta or 0
|
||||
local alt_rel = ""
|
||||
if alt_d > 400.0 then alt_rel = "low"
|
||||
elseif alt_d < -400.0 then alt_rel = "high"
|
||||
end
|
||||
|
||||
local function emit(fmt, ...)
|
||||
tx.token_str = string.format(fmt, ...)
|
||||
end
|
||||
|
||||
if phase == "ENTRY" then
|
||||
tx.priority = 100
|
||||
emit("MERGE_ENTRY|brg=%d|dir_rl=%s|delay=%.1f", brg, dir_rl, delay)
|
||||
|
||||
elseif phase == "OVERSHOOT" then
|
||||
emit("MERGE_OVERSHOOT|brg=%d|dir_rl=%s|alt_rel=%s|delay=%.1f",
|
||||
brg, dir_rl, alt_rel, delay)
|
||||
|
||||
elseif phase == "SEPARATION" then
|
||||
if (ctx.pass_count or 0) < 3 then
|
||||
emit("MERGE_REATTACK|brg=%d|rng=%d|delay=%.1f", brg, rng_km, delay)
|
||||
else
|
||||
emit("ABORT_THREAT|hdg=%d|delay=%.1f", brg, delay)
|
||||
end
|
||||
|
||||
elseif phase == "REATTACK" then
|
||||
if prev_phase ~= "REATTACK" or ticks % 3 == 0 then
|
||||
emit("MERGE_REATTACK|brg=%d|rng=%d|delay=%.1f", brg, rng_km, delay)
|
||||
else
|
||||
tx.silence = true
|
||||
end
|
||||
|
||||
elseif phase == "LOST" then
|
||||
if prev_phase ~= "LOST" or ticks == 4 then
|
||||
emit("MERGE_LOST|brg=%d|rng=%d|delay=%.1f", brg, rng_km, delay)
|
||||
else
|
||||
tx.silence = true
|
||||
end
|
||||
|
||||
elseif phase == "SPLASH" then
|
||||
tx.delay_sec = 1.5
|
||||
tx.priority = 100
|
||||
emit("MERGE_SPLASH|delay=1.5")
|
||||
|
||||
else
|
||||
tx.silence = true
|
||||
end
|
||||
|
||||
return tx
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- END of Class
|
||||
-------------------------------------------------------------------------------
|
||||
@@ -0,0 +1,292 @@
|
||||
--- **Ops** - Passive strategic territory.
|
||||
--
|
||||
-- **Main Features:**
|
||||
--
|
||||
-- * Associate a static MOOSE zone with a coalition
|
||||
-- * Keep strategic territory geometry separate from OPSZONE scanning
|
||||
-- * Register territories in the MOOSE database
|
||||
-- * Delegate coordinate checks and F10 drawing to the underlying zone
|
||||
--
|
||||
-- A TERRITORY is deliberately passive. It does not scan DCS objects, evaluate
|
||||
-- ownership, run a scheduler, or make tactical or strategic decisions.
|
||||
--
|
||||
-- ===
|
||||
--
|
||||
-- @module Ops.Territory
|
||||
|
||||
|
||||
--- TERRITORY class.
|
||||
-- @type TERRITORY
|
||||
-- @field #string ClassName Name of the class.
|
||||
-- @field #string version Class version.
|
||||
-- @field #number verbose Verbosity level.
|
||||
-- @field #string lid Log ID string.
|
||||
-- @field #string name Unique territory name.
|
||||
-- @field #string zoneName Name of the underlying MOOSE zone.
|
||||
-- @field Core.Zone#ZONE_BASE zone Underlying MOOSE zone.
|
||||
-- @field #number coalition Coalition associated with the territory.
|
||||
-- @extends Core.Base#BASE
|
||||
|
||||
--- A passive strategic area defined by an existing MOOSE zone.
|
||||
--
|
||||
-- TERRITORY contains geometry and declarative ownership only. Unlike
|
||||
-- @{Ops.OpsZone#OPSZONE}, it performs no periodic object scans and has no FSM.
|
||||
--
|
||||
-- @field #TERRITORY
|
||||
TERRITORY = {
|
||||
ClassName = "TERRITORY",
|
||||
verbose = 0,
|
||||
}
|
||||
|
||||
--- TERRITORY class version.
|
||||
-- @field #string version
|
||||
TERRITORY.version = "0.1.0"
|
||||
|
||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- DATABASE extension
|
||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Finds a TERRITORY based on its name.
|
||||
-- @param Core.Database#DATABASE self
|
||||
-- @param #string TerritoryName Name of the territory.
|
||||
-- @return #TERRITORY The found territory or `nil`.
|
||||
function DATABASE:FindTerritory(TerritoryName)
|
||||
local territories = self.TERRITORIES or {}
|
||||
return territories[TerritoryName]
|
||||
end
|
||||
|
||||
--- Adds a TERRITORY to the database.
|
||||
-- @param Core.Database#DATABASE self
|
||||
-- @param #TERRITORY Territory Territory to add.
|
||||
-- @return #TERRITORY The registered territory or `nil`.
|
||||
function DATABASE:AddTerritory(Territory)
|
||||
if not Territory then
|
||||
return nil
|
||||
end
|
||||
|
||||
self.TERRITORIES = self.TERRITORIES or {}
|
||||
|
||||
local territoryName = Territory:GetName()
|
||||
if not self.TERRITORIES[territoryName] then
|
||||
self.TERRITORIES[territoryName] = Territory
|
||||
end
|
||||
|
||||
return self.TERRITORIES[territoryName]
|
||||
end
|
||||
|
||||
--- Deletes a TERRITORY from the database.
|
||||
-- @param Core.Database#DATABASE self
|
||||
-- @param #string TerritoryName Name of the territory.
|
||||
-- @return Core.Database#DATABASE self
|
||||
function DATABASE:DeleteTerritory(TerritoryName)
|
||||
if self.TERRITORIES then
|
||||
self.TERRITORIES[TerritoryName] = nil
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Constructor
|
||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Create a new TERRITORY class object.
|
||||
-- @param #TERRITORY self
|
||||
-- @param Core.Zone#ZONE_BASE Zone The underlying MOOSE zone or its Mission Editor name.
|
||||
-- @param #number Coalition (Optional) Associated coalition. Default `coalition.side.NEUTRAL`.
|
||||
-- @param #string Name (Optional) Unique territory name. Default is the zone name.
|
||||
-- @return #TERRITORY self
|
||||
-- @usage
|
||||
-- local north = TERRITORY:New("Territory North", coalition.side.BLUE)
|
||||
-- local southZone = ZONE:FindByName("Territory South")
|
||||
-- local south = TERRITORY:New(southZone, coalition.side.RED, "Southern Territory")
|
||||
function TERRITORY:New(Zone, Coalition, Name)
|
||||
|
||||
-- Inherit everything from BASE class.
|
||||
local self = BASE:Inherit(self, BASE:New()) -- #TERRITORY
|
||||
|
||||
-- Resolve a Mission Editor zone name.
|
||||
if type(Zone) == "string" then
|
||||
local zoneName = Zone
|
||||
Zone = ZONE:FindByName(zoneName)
|
||||
if not Zone then
|
||||
self:E(string.format("ERROR: No ZONE found for name: %s", tostring(zoneName)))
|
||||
return nil
|
||||
end
|
||||
elseif not Zone then
|
||||
self:E("ERROR: First parameter Zone is nil in TERRITORY:New(Zone) call!")
|
||||
return nil
|
||||
end
|
||||
|
||||
-- A territory relies only on the common ZONE_BASE interface.
|
||||
if type(Zone.GetName) ~= "function"
|
||||
or type(Zone.GetCoordinate) ~= "function"
|
||||
or type(Zone.IsCoordinateInZone) ~= "function" then
|
||||
self:E("ERROR: TERRITORY requires a ZONE_BASE derived object!")
|
||||
return nil
|
||||
end
|
||||
|
||||
local zoneName = Zone:GetName()
|
||||
local territoryName = Name or zoneName
|
||||
if type(territoryName) ~= "string" or territoryName == "" then
|
||||
self:E("ERROR: TERRITORY requires a non-empty name!")
|
||||
return nil
|
||||
end
|
||||
|
||||
self.zone = Zone
|
||||
self.zoneName = zoneName
|
||||
self.name = territoryName
|
||||
self.lid = string.format("TERRITORY %s | ", territoryName)
|
||||
|
||||
if not self:SetCoalition(Coalition or coalition.side.NEUTRAL) then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Register in the MOOSE database.
|
||||
_DATABASE:AddTerritory(self)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Set functions
|
||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Set the coalition associated with this territory.
|
||||
-- This method only changes declarative ownership. It does not evaluate DCS
|
||||
-- units or trigger any capture logic.
|
||||
-- @param #TERRITORY self
|
||||
-- @param #number Coalition Coalition side number.
|
||||
-- @return #TERRITORY self or `nil` if the coalition is invalid.
|
||||
function TERRITORY:SetCoalition(Coalition)
|
||||
if Coalition ~= coalition.side.NEUTRAL
|
||||
and Coalition ~= coalition.side.RED
|
||||
and Coalition ~= coalition.side.BLUE then
|
||||
self:E(self.lid .. string.format("ERROR: Invalid coalition: %s", tostring(Coalition)))
|
||||
return nil
|
||||
end
|
||||
|
||||
self.coalition = Coalition
|
||||
return self
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Get functions
|
||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Find a TERRITORY by name.
|
||||
-- @param #TERRITORY self
|
||||
-- @param #string Name Name of the territory.
|
||||
-- @return #TERRITORY The found territory or `nil`.
|
||||
function TERRITORY:FindByName(Name)
|
||||
return _DATABASE:FindTerritory(Name)
|
||||
end
|
||||
|
||||
--- Get the territory name.
|
||||
-- @param #TERRITORY self
|
||||
-- @return #string Territory name.
|
||||
function TERRITORY:GetName()
|
||||
return self.name
|
||||
end
|
||||
|
||||
--- Get the underlying zone name.
|
||||
-- @param #TERRITORY self
|
||||
-- @return #string Zone name.
|
||||
function TERRITORY:GetZoneName()
|
||||
return self.zoneName
|
||||
end
|
||||
|
||||
--- Get the underlying MOOSE zone.
|
||||
-- @param #TERRITORY self
|
||||
-- @return Core.Zone#ZONE_BASE The underlying zone.
|
||||
function TERRITORY:GetZone()
|
||||
return self.zone
|
||||
end
|
||||
|
||||
--- Get the territory center coordinate.
|
||||
-- @param #TERRITORY self
|
||||
-- @return Core.Point#COORDINATE Territory center coordinate.
|
||||
function TERRITORY:GetCoordinate()
|
||||
return self.zone:GetCoordinate()
|
||||
end
|
||||
|
||||
--- Get the coalition associated with the territory.
|
||||
-- @param #TERRITORY self
|
||||
-- @return #number Coalition side number.
|
||||
function TERRITORY:GetCoalition()
|
||||
return self.coalition
|
||||
end
|
||||
|
||||
--- Get the name of the coalition associated with the territory.
|
||||
-- @param #TERRITORY self
|
||||
-- @return #string Coalition name.
|
||||
function TERRITORY:GetCoalitionName()
|
||||
return UTILS.GetCoalitionName(self.coalition)
|
||||
end
|
||||
|
||||
--- Get the owner of the territory.
|
||||
-- This is an alias for @{#TERRITORY.GetCoalition}.
|
||||
-- @param #TERRITORY self
|
||||
-- @return #number Coalition side number.
|
||||
function TERRITORY:GetOwner()
|
||||
return self:GetCoalition()
|
||||
end
|
||||
|
||||
--- Get the owner coalition name.
|
||||
-- This is an alias for @{#TERRITORY.GetCoalitionName}.
|
||||
-- @param #TERRITORY self
|
||||
-- @return #string Coalition name.
|
||||
function TERRITORY:GetOwnerName()
|
||||
return self:GetCoalitionName()
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Zone functions
|
||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Test whether a coordinate lies inside the territory.
|
||||
-- @param #TERRITORY self
|
||||
-- @param Core.Point#COORDINATE Coordinate Coordinate to test.
|
||||
-- @return #boolean `true` if the coordinate lies inside the territory.
|
||||
function TERRITORY:ContainsCoordinate(Coordinate)
|
||||
return self.zone:IsCoordinateInZone(Coordinate)
|
||||
end
|
||||
|
||||
--- Test whether a Vec2 lies inside the territory.
|
||||
-- @param #TERRITORY self
|
||||
-- @param DCS#Vec2 Vec2 Vec2 to test.
|
||||
-- @return #boolean `true` if the Vec2 lies inside the territory.
|
||||
function TERRITORY:ContainsVec2(Vec2)
|
||||
return self.zone:IsVec2InZone(Vec2)
|
||||
end
|
||||
|
||||
--- Draw the territory on the F10 map.
|
||||
-- Drawing is delegated to the underlying MOOSE zone.
|
||||
-- @param #TERRITORY self
|
||||
-- @param #number Coalition (Optional) Coalition visibility. Default `-1` for all.
|
||||
-- @param #table Color (Optional) RGB line color.
|
||||
-- @param #number Alpha (Optional) Line alpha.
|
||||
-- @param #table FillColor (Optional) RGB fill color.
|
||||
-- @param #number FillAlpha (Optional) Fill alpha.
|
||||
-- @param #number LineType (Optional) DCS line type.
|
||||
-- @return #TERRITORY self
|
||||
function TERRITORY:Draw(Coalition, Color, Alpha, FillColor, FillAlpha, LineType)
|
||||
self.zone:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Remove the territory drawing from the F10 map.
|
||||
-- @param #TERRITORY self
|
||||
-- @return #TERRITORY self
|
||||
function TERRITORY:Undraw()
|
||||
self.zone:UndrawZone()
|
||||
return self
|
||||
end
|
||||
|
||||
--- Remove the territory from the MOOSE database.
|
||||
-- The underlying zone is not deleted.
|
||||
-- @param #TERRITORY self
|
||||
-- @return #TERRITORY self
|
||||
function TERRITORY:Remove()
|
||||
_DATABASE:DeleteTerritory(self.name)
|
||||
return self
|
||||
end
|
||||
@@ -79,11 +79,7 @@ __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/Tiresias.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/Stratego.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/ClientWatch.lua')
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/Formation.lua')
|
||||
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/RedGCIKernel.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/RedGCI.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/RedGCI2v2.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/RedGCIDispatcher.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/Territory.lua')
|
||||
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Airboss.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/RecoveryTanker.lua' )
|
||||
@@ -142,4 +138,13 @@ __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Navigation/Beacons.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Navigation/Radios.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Navigation/Towns.lua' )
|
||||
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Python/MooseBridgeJson.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Python/MooseBridge.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Python/MooseBridgeSocketTuningExtension.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Python/MooseBridgeDcsEventsExtension.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Python/MooseBridgeAuftragExecutionExtension.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Python/MooseBridgeAuftragTraceExtension.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Python/MooseBridgeIntelExtension.lua' )
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Python/MooseBridgePayloadExtension.lua' )
|
||||
|
||||
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Globals.lua' )
|
||||
|
||||
@@ -290,6 +290,9 @@ function AIRWING:New(warehousename, airwingname)
|
||||
-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup The FLIGHTGROUP on mission.
|
||||
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
|
||||
|
||||
-- Add legion to DB
|
||||
_DATABASE:AddLegion(self)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
@@ -431,7 +434,15 @@ function AIRWING:NewPayload(Unit, Npayloads, MissionTypes, Performance)
|
||||
capability.MissionType=AUFTRAG.Type.RELOCATECOHORT
|
||||
capability.Performance=50
|
||||
table.insert(payload.capabilities, capability)
|
||||
end
|
||||
end
|
||||
|
||||
-- Add OPSTRANSPORT for all.
|
||||
if not AUFTRAG.CheckMissionType(AUFTRAG.Type.OPSTRANSPORT, MissionTypes) then
|
||||
local capability={} --Ops.Auftrag#AUFTRAG.Capability
|
||||
capability.MissionType=AUFTRAG.Type.OPSTRANSPORT
|
||||
capability.Performance=50
|
||||
table.insert(payload.capabilities, capability)
|
||||
end
|
||||
|
||||
-- Info
|
||||
self:T(self.lid..string.format("Adding new payload from unit %s for aircraft type %s: ID=%d, N=%d (unlimited=%s), performance=%d, missions: %s",
|
||||
@@ -541,6 +552,47 @@ function AIRWING:AddPayloadCapability(Payload, MissionTypes, Performance)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Filter available payloads for a given aircraft type and mission type.
|
||||
-- @param #AIRWING self
|
||||
-- @param #string UnitType The type of the unit.
|
||||
-- @param #string MissionType The mission type.
|
||||
-- @param #table Payloads Specific payloads only to be considered.
|
||||
-- @return #AIRWING.Payload Payload table or *nil*.
|
||||
function AIRWING:_FilterPlayloads(UnitType, MissionType, Payloads)
|
||||
|
||||
local function _checkPayloads(payload)
|
||||
if Payloads then
|
||||
for _,Payload in pairs(Payloads) do
|
||||
if Payload.uid==payload.uid then
|
||||
return true
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Payload was not specified.
|
||||
return nil
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Pre-selection: filter out only those payloads that are valid for the airframe and mission type and are available.
|
||||
local payloads={}
|
||||
|
||||
for _,_payload in pairs(self.payloads) do
|
||||
local payload=_payload --#AIRWING.Payload
|
||||
|
||||
local specialpayload=_checkPayloads(payload)
|
||||
local compatible=AUFTRAG.CheckMissionCapability(MissionType, payload.capabilities)
|
||||
|
||||
local goforit = specialpayload or (specialpayload==nil and compatible)
|
||||
|
||||
if payload.aircrafttype==UnitType and payload.navail>0 and goforit then
|
||||
table.insert(payloads, payload)
|
||||
end
|
||||
end
|
||||
|
||||
return payloads
|
||||
end
|
||||
|
||||
--- Fetch a payload from the airwing resources for a given unit and mission type.
|
||||
-- The payload with the highest priority is preferred.
|
||||
-- @param #AIRWING self
|
||||
@@ -589,34 +641,7 @@ function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads)
|
||||
end
|
||||
end
|
||||
|
||||
local function _checkPayloads(payload)
|
||||
if Payloads then
|
||||
for _,Payload in pairs(Payloads) do
|
||||
if Payload.uid==payload.uid then
|
||||
return true
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Payload was not specified.
|
||||
return nil
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Pre-selection: filter out only those payloads that are valid for the airframe and mission type and are available.
|
||||
local payloads={}
|
||||
for _,_payload in pairs(self.payloads) do
|
||||
local payload=_payload --#AIRWING.Payload
|
||||
|
||||
local specialpayload=_checkPayloads(payload)
|
||||
local compatible=AUFTRAG.CheckMissionCapability(MissionType, payload.capabilities)
|
||||
|
||||
local goforit = specialpayload or (specialpayload==nil and compatible)
|
||||
|
||||
if payload.aircrafttype==UnitType and payload.navail>0 and goforit then
|
||||
table.insert(payloads, payload)
|
||||
end
|
||||
end
|
||||
local payloads=self:_FilterPlayloads(UnitType, MissionType, Payloads)
|
||||
|
||||
-- Debug.
|
||||
if self.verbose>=4 then
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
-- **Supported Aircraft:**
|
||||
--
|
||||
-- * [F/A-18C Hornet Lot 20](https://forums.eagle.ru/forumdisplay.php?f=557) (Player & AI)
|
||||
-- * [F-14A/B/A Early Tomcat](https://forums.eagle.ru/forumdisplay.php?f=395) (Player & AI)
|
||||
-- * [F-14A/B/B(U)/A Early Tomcat](https://forums.eagle.ru/forumdisplay.php?f=395) (Player & AI)
|
||||
-- * [A-4E Skyhawk Community Mod](https://forums.eagle.ru/showthread.php?t=224989) (Player & AI)
|
||||
-- * [AV-8B N/A Harrier](https://forums.eagle.ru/forumdisplay.php?f=555) (Player & AI)
|
||||
-- * [T-45C Goshawk](https://forum.dcs.world/topic/203816-vnao-t-45-goshawk/) (VNAO mod) (Player & AI)
|
||||
@@ -1276,6 +1276,7 @@ AIRBOSS = {
|
||||
-- @field #string A4EC A-4E Community mod.
|
||||
-- @field #string HORNET F/A-18C Lot 20 Hornet by Eagle Dynamics.
|
||||
-- @field #string F14A F-14A by Heatblur.
|
||||
-- @field #string F14BU F-14BU by Heatblur.
|
||||
-- @field #string F14A_Early F-14A-135-GR-Early by Heatblur.
|
||||
-- @field #string F14B F-14B by Heatblur.
|
||||
-- @field #string F14A_AI F-14A Tomcat (AI).
|
||||
@@ -1297,6 +1298,7 @@ AIRBOSS.AircraftCarrier={
|
||||
F14A="F-14A-135-GR",
|
||||
F14A_Early="F-14A-135-GR-Early",
|
||||
F14B="F-14B",
|
||||
F14BU="F-14BU",
|
||||
F14A_AI="F-14A",
|
||||
FA18C="F/A-18C",
|
||||
T45C="T-45",
|
||||
@@ -5508,7 +5510,7 @@ function AIRBOSS:_GetAircraftAoA( playerData )
|
||||
local goshawk = playerData.actype == AIRBOSS.AircraftCarrier.T45C
|
||||
local skyhawk = playerData.actype == AIRBOSS.AircraftCarrier.A4EC
|
||||
local harrier = playerData.actype == AIRBOSS.AircraftCarrier.AV8B
|
||||
local tomcat = playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B or playerData.actype == AIRBOSS.AircraftCarrier.F14A_Early
|
||||
local tomcat = self:_IsTomcat( playerData.actype )
|
||||
local corsair = playerData.actype == AIRBOSS.AircraftCarrier.CORSAIR or playerData.actype == AIRBOSS.AircraftCarrier.CORSAIR_CW
|
||||
|
||||
-- Table with AoA values.
|
||||
@@ -5571,6 +5573,18 @@ function AIRBOSS:_GetAircraftAoA( playerData )
|
||||
aoa.OnSpeedMin = 9.5
|
||||
aoa.Fast = 8.0
|
||||
aoa.FAST = 7.5
|
||||
else
|
||||
-- Unknown carrier-capable type: _IsCarrierAircraft() is enum-driven, so a type can
|
||||
-- reach here with no AoA branch. Returning an empty table nil-propagates into
|
||||
-- _GetAircraftParameters() and the AoA waveoff check. Fall back to Hornet values.
|
||||
self:E( self.lid .. string.format( "ERROR: No AoA parameters defined for aircraft type %s! Falling back to F/A-18C values.", tostring( playerData.actype ) ) )
|
||||
aoa.SLOW = 9.8
|
||||
aoa.Slow = 9.3
|
||||
aoa.OnSpeedMax = 8.8
|
||||
aoa.OnSpeed = 8.1
|
||||
aoa.OnSpeedMin = 7.4
|
||||
aoa.Fast = 6.9
|
||||
aoa.FAST = 6.3
|
||||
end
|
||||
|
||||
return aoa
|
||||
@@ -5587,7 +5601,7 @@ function AIRBOSS:_AoAUnit2Deg( playerData, aoaunits )
|
||||
local degrees = aoaunits
|
||||
|
||||
-- Check aircraft type of player.
|
||||
if playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B or playerData.actype == AIRBOSS.AircraftCarrier.F14A_Early then
|
||||
if self:_IsTomcat( playerData.actype ) then
|
||||
|
||||
-------------
|
||||
-- F-14A/B --
|
||||
@@ -5630,7 +5644,7 @@ function AIRBOSS:_AoADeg2Units( playerData, degrees )
|
||||
local aoaunits = degrees
|
||||
|
||||
-- Check aircraft type of player.
|
||||
if playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B or playerData.actype == AIRBOSS.AircraftCarrier.F14A_Early then
|
||||
if self:_IsTomcat( playerData.actype ) then
|
||||
|
||||
-------------
|
||||
-- F-14A/B --
|
||||
@@ -5680,7 +5694,7 @@ function AIRBOSS:_GetAircraftParameters( playerData, step )
|
||||
or playerData.actype == AIRBOSS.AircraftCarrier.RHINOF
|
||||
or playerData.actype == AIRBOSS.AircraftCarrier.GROWLER
|
||||
local skyhawk = playerData.actype == AIRBOSS.AircraftCarrier.A4EC
|
||||
local tomcat = playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B
|
||||
local tomcat = self:_IsTomcat( playerData.actype )
|
||||
local harrier = playerData.actype == AIRBOSS.AircraftCarrier.AV8B
|
||||
local goshawk = playerData.actype == AIRBOSS.AircraftCarrier.T45C
|
||||
local corsair = playerData.actype == AIRBOSS.AircraftCarrier.CORSAIR or playerData.actype == AIRBOSS.AircraftCarrier.CORSAIR_CW
|
||||
@@ -6613,9 +6627,7 @@ function AIRBOSS:_RefuelAI( flight )
|
||||
local refuelac=false
|
||||
local actype=flight.group:GetTypeName()
|
||||
if actype==AIRBOSS.AircraftCarrier.AV8B or
|
||||
actype==AIRBOSS.AircraftCarrier.F14A or
|
||||
actype==AIRBOSS.AircraftCarrier.F14B or
|
||||
actype==AIRBOSS.AircraftCarrier.F14A_AI or
|
||||
self:_IsTomcat( actype, true ) or
|
||||
actype==AIRBOSS.AircraftCarrier.HORNET or
|
||||
actype==AIRBOSS.AircraftCarrier.RHINOE or
|
||||
actype==AIRBOSS.AircraftCarrier.RHINOF or
|
||||
@@ -6725,7 +6737,7 @@ function AIRBOSS:_LandAI( flight )
|
||||
Speed = UTILS.KnotsToKmph( 200 )
|
||||
elseif flight.actype == AIRBOSS.AircraftCarrier.E2D or flight.actype == AIRBOSS.AircraftCarrier.C2A then
|
||||
Speed = UTILS.KnotsToKmph( 150 )
|
||||
elseif flight.actype == AIRBOSS.AircraftCarrier.F14A_AI or flight.actype == AIRBOSS.AircraftCarrier.F14A or flight.actype == AIRBOSS.AircraftCarrier.F14B then
|
||||
elseif self:_IsTomcat( flight.actype, true ) then
|
||||
Speed = UTILS.KnotsToKmph( 175 )
|
||||
elseif flight.actype == AIRBOSS.AircraftCarrier.S3B or flight.actype == AIRBOSS.AircraftCarrier.S3BTANKER then
|
||||
Speed = UTILS.KnotsToKmph( 140 )
|
||||
@@ -8200,7 +8212,7 @@ function AIRBOSS:_CheckPlayerStatus()
|
||||
or playerData.actype == AIRBOSS.AircraftCarrier.RHINOE
|
||||
or playerData.actype == AIRBOSS.AircraftCarrier.RHINOF
|
||||
or playerData.actype == AIRBOSS.AircraftCarrier.GROWLER
|
||||
local tomcat = playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B
|
||||
local tomcat = self:_IsTomcat( playerData.actype )
|
||||
|
||||
-- VNAO Edit - Added wrapped up call to LSO grading Hornet
|
||||
if playerData.step==AIRBOSS.PatternStep.WAKE and hornet then-- VNAO Edit - Added
|
||||
@@ -9455,7 +9467,7 @@ function AIRBOSS:_Initial( playerData )
|
||||
|
||||
-- Hook down for students.
|
||||
if playerData.difficulty == AIRBOSS.Difficulty.EASY and playerData.actype ~= AIRBOSS.AircraftCarrier.AV8B then
|
||||
if playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then
|
||||
if self:_IsTomcat( playerData.actype ) then
|
||||
hint = hint .. " - Hook down, SAS on, Wing Sweep 68°!"
|
||||
else
|
||||
hint = hint .. " - Hook down!"
|
||||
@@ -9620,8 +9632,7 @@ function AIRBOSS:_DirtyUp( playerData )
|
||||
|
||||
-- Radio call "Say/Fly needles". Delayed by 10/15 seconds.
|
||||
if playerData.actype == AIRBOSS.AircraftCarrier.HORNET
|
||||
or playerData.actype == AIRBOSS.AircraftCarrier.F14A
|
||||
or playerData.actype == AIRBOSS.AircraftCarrier.F14B
|
||||
or self:_IsTomcat( playerData.actype )
|
||||
or playerData.actype == AIRBOSS.AircraftCarrier.RHINOE
|
||||
or playerData.actype == AIRBOSS.AircraftCarrier.RHINOF
|
||||
or playerData.actype == AIRBOSS.AircraftCarrier.GROWLER
|
||||
@@ -10815,7 +10826,7 @@ function AIRBOSS:_Trapped( playerData )
|
||||
or playerData.actype == AIRBOSS.AircraftCarrier.RHINOF
|
||||
or playerData.actype == AIRBOSS.AircraftCarrier.GROWLER then
|
||||
dcorr = 100
|
||||
elseif playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then
|
||||
elseif self:_IsTomcat( playerData.actype ) then
|
||||
-- TODO: Check Tomcat.
|
||||
dcorr = 100
|
||||
elseif playerData.actype == AIRBOSS.AircraftCarrier.A4EC then
|
||||
@@ -11628,7 +11639,7 @@ function AIRBOSS:_AttitudeMonitor( playerData )
|
||||
|
||||
local unitClient = Unit.getByName(unit:GetName()) -- VNAO Edit - Added
|
||||
local hornet = playerData.actype == AIRBOSS.AircraftCarrier.HORNET -- VNAO Edit - Added
|
||||
local tomcat = playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B or playerData.actype == AIRBOSS.AircraftCarrier.F14A_Early -- VNAO Edit - Added
|
||||
local tomcat = self:_IsTomcat( playerData.actype ) -- VNAO Edit - Added
|
||||
|
||||
if hornet then -- VNAO Edit - Added
|
||||
local nozzlePosL = 0 -- VNAO Edit - Added
|
||||
@@ -11861,7 +11872,7 @@ function AIRBOSS:_NozzleArgumentLeft( unit ) -- VNAO Edit - Added
|
||||
else -- VNAO Edit - Added
|
||||
nozzlePosL = 0 -- VNAO Edit - Added
|
||||
end -- VNAO Edit - Added
|
||||
elseif typeName == "F-14A-135-GR" or typeName == "F-14B" or typeName == "F-14A-135-GR-Early" then -- VNAO Edit - Added
|
||||
elseif self:_IsTomcat( typeName ) then -- VNAO Edit - Added
|
||||
nozzlePosL = unitClient:getDrawArgumentValue(434) -- VNAO Edit - Added
|
||||
end -- VNAO Edit - Added
|
||||
|
||||
@@ -11886,7 +11897,7 @@ function AIRBOSS:_NozzleArgumentRight( unit ) -- VNAO Edit - Added
|
||||
else -- VNAO Edit - Added
|
||||
nozzlePosR = 0 -- VNAO Edit - Added
|
||||
end -- VNAO Edit - Added
|
||||
elseif typeName == "F-14A-135-GR" or typeName == "F-14B" or typeName == "F-14A-135-GR-Early" then -- VNAO Edit - Added
|
||||
elseif self:_IsTomcat( typeName ) then -- VNAO Edit - Added
|
||||
nozzlePosR = unitClient:getDrawArgumentValue(433) -- VNAO Edit - Added
|
||||
end -- VNAO Edit - Added
|
||||
return nozzlePosR -- VNAO Edit - Added
|
||||
@@ -13149,7 +13160,7 @@ function AIRBOSS:_Flightdata2Text( playerData, groovestep )
|
||||
|
||||
|
||||
local hornet = playerData.actype == AIRBOSS.AircraftCarrier.HORNET-- VNAO Edit - Added
|
||||
local tomcat = playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B-- VNAO Edit - Added
|
||||
local tomcat = self:_IsTomcat( playerData.actype )-- VNAO Edit - Added
|
||||
|
||||
if hornet then-- VNAO Edit - Added
|
||||
if Lnoz > 0.6 and Rnoz > 0.6 then -- VNAO Edit - Added check them both, it's possilbe there could be a single engine landing and one is in idle perhaps?
|
||||
@@ -13665,7 +13676,7 @@ function AIRBOSS:_StepHint( playerData, step )
|
||||
|
||||
-- Late break.
|
||||
if step == AIRBOSS.PatternStep.LATEBREAK then
|
||||
if playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then
|
||||
if self:_IsTomcat( playerData.actype ) then
|
||||
hint = hint .. "\nWing Sweep 20°, Gear DOWN < 280 KIAS."
|
||||
end
|
||||
end
|
||||
@@ -13674,7 +13685,7 @@ function AIRBOSS:_StepHint( playerData, step )
|
||||
if step == AIRBOSS.PatternStep.ABEAM then
|
||||
if playerData.actype == AIRBOSS.AircraftCarrier.AV8B then
|
||||
hint = hint .. "\nNozzles 50°-60°. Antiskid OFF. Lights OFF."
|
||||
elseif playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then
|
||||
elseif self:_IsTomcat( playerData.actype ) then
|
||||
hint = hint .. "\nSlats/Flaps EXTENDED < 225 KIAS. DLC SELECTED. Auto Throttle IF DESIRED."
|
||||
else
|
||||
hint = hint .. "\nDirty up! Gear DOWN, flaps DOWN. Check hook down."
|
||||
@@ -15109,7 +15120,7 @@ function AIRBOSS:_GetACNickname( actype )
|
||||
nickname = "Hawkeye"
|
||||
elseif actype == AIRBOSS.AircraftCarrier.C2A then
|
||||
nickname = "Greyhound"
|
||||
elseif actype == AIRBOSS.AircraftCarrier.F14A_AI or actype == AIRBOSS.AircraftCarrier.F14A or actype == AIRBOSS.AircraftCarrier.F14B or actype == AIRBOSS.AircraftCarrier.F14A_Early then
|
||||
elseif self:_IsTomcat( actype, true ) then
|
||||
nickname = "Tomcat"
|
||||
elseif actype == AIRBOSS.AircraftCarrier.FA18C or actype == AIRBOSS.AircraftCarrier.HORNET then
|
||||
nickname = "Hornet"
|
||||
@@ -15246,6 +15257,29 @@ function AIRBOSS:_GetGoodBadScore( playerData )
|
||||
return lowscore, badscore
|
||||
end
|
||||
|
||||
--- Check whether an aircraft type name is any F-14 Tomcat variant.
|
||||
-- Central predicate so that adding a new Tomcat variant only requires editing
|
||||
-- this function and the AIRBOSS.AircraftCarrier enum.
|
||||
-- @param #AIRBOSS self
|
||||
-- @param #string actype Aircraft type name, e.g. from `unit:GetTypeName()` or `playerData.actype`.
|
||||
-- @param #boolean IncludeAI If true, also match the AI-only F-14A (AIRBOSS.AircraftCarrier.F14A_AI).
|
||||
-- @return #boolean If true, actype is an F-14 variant.
|
||||
function AIRBOSS:_IsTomcat( actype, IncludeAI )
|
||||
|
||||
if actype == AIRBOSS.AircraftCarrier.F14A
|
||||
or actype == AIRBOSS.AircraftCarrier.F14B
|
||||
or actype == AIRBOSS.AircraftCarrier.F14BU
|
||||
or actype == AIRBOSS.AircraftCarrier.F14A_Early then
|
||||
return true
|
||||
end
|
||||
|
||||
if IncludeAI and actype == AIRBOSS.AircraftCarrier.F14A_AI then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--- Check if aircraft is capable of landing on this aircraft carrier.
|
||||
-- @param #AIRBOSS self
|
||||
-- @param Wrapper.Unit#UNIT unit Aircraft unit. (Will also work with groups as given parameter.)
|
||||
@@ -19394,4 +19428,4 @@ function AIRBOSS:onafterLSOGrade(From, Event, To, playerData, grade)
|
||||
self.funkmanSocket:SendTable(result)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -191,6 +191,8 @@
|
||||
-- @field #boolean optionEmission Emission is on or off.
|
||||
-- @field #boolean optionInvisible Invisible is on/off.
|
||||
-- @field #boolean optionImmortal Immortal is on/off.
|
||||
--
|
||||
-- @field #AUFTRAG.Summary summary Auftrag summary.
|
||||
--
|
||||
-- @extends Core.Fsm#FSM
|
||||
|
||||
@@ -656,6 +658,19 @@ AUFTRAG.Category={
|
||||
-- @field #string DAMAGED Target was damaged.
|
||||
-- @field #string DESTROYED Target was destroyed.
|
||||
|
||||
--- Mission summary.
|
||||
-- @type AUFTRAG.Summary
|
||||
-- @field #boolean success If true, mission was successful.
|
||||
-- @field #number Ntargets0 Number of initial targets.
|
||||
-- @field #number Ntargets Number of final targets after mission is done.
|
||||
-- @field #number damage Target damage in per cent.
|
||||
-- @field #number Ndestroyed Number of destroyed targets.
|
||||
-- @field #number Nkills Number of kills from assigned groups.
|
||||
-- @field #number Nelements Number of elements assigned to mission.
|
||||
-- @field #number targetLife Target life points after mission is over.
|
||||
-- @field #number category Target category.
|
||||
-- @field #number Ncasualties Number of own casualties.
|
||||
|
||||
--- Generic mission condition.
|
||||
-- @type AUFTRAG.Condition
|
||||
-- @field #function func Callback function to check for a condition. Should return a #boolean.
|
||||
@@ -676,7 +691,7 @@ AUFTRAG.Category={
|
||||
|
||||
--- AUFTRAG class version.
|
||||
-- @field #string version
|
||||
AUFTRAG.version="1.4.2"
|
||||
AUFTRAG.version="1.5.0"
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- TODO list
|
||||
@@ -776,6 +791,7 @@ function AUFTRAG:New(Type)
|
||||
|
||||
self:AddTransition("*", "Cancel", AUFTRAG.Status.CANCELLED) -- Command to cancel the mission.
|
||||
|
||||
self:AddTransition("*", "Evaluated", "*")
|
||||
self:AddTransition("*", "Success", AUFTRAG.Status.SUCCESS)
|
||||
self:AddTransition("*", "Failed", AUFTRAG.Status.FAILED)
|
||||
|
||||
@@ -946,6 +962,24 @@ function AUFTRAG:New(Type)
|
||||
-- @param #string Event Event.
|
||||
-- @param #string To To state.
|
||||
|
||||
--- Triggers the FSM event "Evaluated".
|
||||
-- @function [parent=#AUFTRAG] Evaluated
|
||||
-- @param #AUFTRAG self
|
||||
-- @param #AUFTRAG.Summary
|
||||
|
||||
--- Triggers the FSM event "Evaluated" after a delay.
|
||||
-- @function [parent=#AUFTRAG] __Evaluated
|
||||
-- @param #AUFTRAG self
|
||||
-- @param #number delay Delay in seconds.
|
||||
-- @param #AUFTRAG.Summary Summary Mission summary.
|
||||
|
||||
--- On after "Evaluated" event.
|
||||
-- @function [parent=#AUFTRAG] OnAfterEvaluated
|
||||
-- @param #AUFTRAG self
|
||||
-- @param #string From From state.
|
||||
-- @param #string Event Event.
|
||||
-- @param #string To To state.
|
||||
-- @param #AUFTRAG.Summary Summary Mission summary.
|
||||
|
||||
--- Triggers the FSM event "Success".
|
||||
-- @function [parent=#AUFTRAG] Success
|
||||
@@ -1750,6 +1784,9 @@ function AUFTRAG:NewBAI(Target, Altitude)
|
||||
mission.missionFraction=0.75
|
||||
mission.optionROE=ENUMS.ROE.OpenFire
|
||||
mission.optionROT=ENUMS.ROT.PassiveDefense
|
||||
|
||||
-- Evaluate result after 5 min. We might need time until the bombs have dropped and targets have been detroyed.
|
||||
mission.dTevaluate=5*60
|
||||
|
||||
mission.categories={AUFTRAG.Category.AIRCRAFT}
|
||||
|
||||
@@ -2303,6 +2340,9 @@ end
|
||||
function AUFTRAG:NewARTY(Target, Nshots, Radius, Altitude)
|
||||
|
||||
local mission=AUFTRAG:New(AUFTRAG.Type.ARTY)
|
||||
|
||||
printf("FF nshots=%s", tostring(Nshots))
|
||||
printf("FF radius=%s", tostring(Radius))
|
||||
|
||||
mission:_TargetFromObject(Target)
|
||||
|
||||
@@ -2407,7 +2447,7 @@ end
|
||||
-- @param #AUFTRAG self
|
||||
-- @param Ops.OpsZone#OPSZONE OpsZone The OPS zone to capture.
|
||||
-- @param #number Coalition The coalition which should capture the zone for the mission to be successful.
|
||||
-- @param #number Speed Speed in knots.
|
||||
-- @param #number Speed (Optional) Speed in knots.
|
||||
-- @param #number Altitude (Optional) Altitude in feet. Only for airborne units. Default 2000 feet ASL.
|
||||
-- @param #string Formation (Optional) Formation used by ground units during patrol. Default "Off Road".
|
||||
-- @param #number StayInZoneTime Stay this many seconds in the zone when done, only then drive back.
|
||||
@@ -4689,6 +4729,18 @@ function AUFTRAG:Evaluate()
|
||||
elseif successCondition then
|
||||
failed=false
|
||||
end
|
||||
|
||||
self.summary={} --#AUFTRAG.Summary
|
||||
self.summary.success=not failed
|
||||
self.summary.damage=targetdamage
|
||||
self.summary.Ntargets=Ntargets
|
||||
self.summary.Ntargets0=Ntargets0
|
||||
self.summary.Ncasualties=self.Ncasualties
|
||||
self.summary.Ndestroyed=self.engageTarget.Ndestroyed
|
||||
self.summary.Nelements=self.Nelements
|
||||
self.summary.Nkills=self.Nkills
|
||||
self.summary.category=self.engageTarget:GetCategory()
|
||||
self.summary.targetLife=Life
|
||||
|
||||
-- Debug text.
|
||||
if self.verbose > 0 then
|
||||
@@ -4708,6 +4760,9 @@ function AUFTRAG:Evaluate()
|
||||
text=text..string.format("=========================")
|
||||
self:I(self.lid..text)
|
||||
end
|
||||
|
||||
-- Trigger evaluated result and pass summary.
|
||||
self:Evaluated(self.summary)
|
||||
|
||||
-- Trigger events.
|
||||
if failed then
|
||||
@@ -5018,7 +5073,7 @@ function AUFTRAG:CheckGroupsDone()
|
||||
-- Check status of all OPS groups.
|
||||
for groupname,data in pairs(self.groupdata) do
|
||||
local groupdata=data --#AUFTRAG.GroupData
|
||||
if groupdata then
|
||||
if groupdata and not groupdata.opsgroup:IsDestroyed() then
|
||||
if not (groupdata.status==AUFTRAG.GroupStatus.DONE or groupdata.status==AUFTRAG.GroupStatus.CANCELLED) then
|
||||
-- At least this group is not DONE or CANCELLED.
|
||||
self:T2(self.lid..string.format("CheckGroupsDone: OPSGROUP %s is not DONE or CANCELLED but in state %s. Mission NOT DONE!", groupdata.opsgroup.groupname, groupdata.status:upper()))
|
||||
@@ -5120,7 +5175,6 @@ end
|
||||
-- FSM Functions
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
--- On after "Planned" event.
|
||||
-- @param #AUFTRAG self
|
||||
-- @param #string From From state.
|
||||
@@ -5430,7 +5484,7 @@ function AUFTRAG:onafterSuccess(From, Event, To)
|
||||
|
||||
-- Stop mission.
|
||||
self:T(self.lid..string.format("Mission SUCCESS! Number of max repeats %d reached ==> Stopping mission!", self.repeated+1))
|
||||
self:Stop()
|
||||
self:__Stop(-120)
|
||||
|
||||
end
|
||||
|
||||
@@ -5472,7 +5526,7 @@ function AUFTRAG:onafterFailed(From, Event, To)
|
||||
|
||||
-- Stop mission.
|
||||
self:T(self.lid..string.format("Mission FAILED! Number of max repeats %d reached ==> Stopping mission!", self.repeated+1))
|
||||
self:Stop()
|
||||
self:__Stop(-120)
|
||||
|
||||
end
|
||||
|
||||
@@ -5585,6 +5639,7 @@ function AUFTRAG:onafterRepeat(From, Event, To)
|
||||
self.Ngroups=0
|
||||
self.Nassigned=nil
|
||||
self.Ndead=0
|
||||
self.summary=nil
|
||||
|
||||
-- Update DCS mission task. Could be that the initial task (e.g. for bombing) was destroyed. Then we need to update the coordinate.
|
||||
self.DCStask=self:GetDCSMissionTask()
|
||||
@@ -5635,6 +5690,8 @@ function AUFTRAG:onafterStop(From, Event, To)
|
||||
|
||||
-- No group data.
|
||||
self.groupdata={}
|
||||
|
||||
self.summary=nil
|
||||
|
||||
-- Clear pending scheduler calls.
|
||||
self.CallScheduler:Clear()
|
||||
@@ -5980,6 +6037,7 @@ function AUFTRAG:CountOpsGroups()
|
||||
local groupdata=_groupdata --#AUFTRAG.GroupData
|
||||
if groupdata and groupdata.opsgroup and groupdata.opsgroup:IsAlive() and not groupdata.opsgroup:IsDead() then
|
||||
N=N+1
|
||||
printf("FF Count Group N=%d %s", N, tostring(groupdata.opsgroup.groupname))
|
||||
end
|
||||
end
|
||||
return N
|
||||
@@ -6466,7 +6524,13 @@ function AUFTRAG:GetDCSMissionTask(MissionGroup)
|
||||
-- ESCORT Mission --
|
||||
--------------------
|
||||
|
||||
local DCStask=CONTROLLABLE.TaskEscort(nil, self.engageTarget:GetObject(), self.escortVec3, nil, self.engageMaxDistance, self.engageTargetTypes)
|
||||
local FollowGroup=self.engageTarget:GetObject()
|
||||
local DCStask=nil
|
||||
if next(self.engageTargetTypes)==nil then
|
||||
DCStask=FollowGroup:TaskFollow(FollowGroup, self.escortVec3)
|
||||
else
|
||||
DCStask=CONTROLLABLE.TaskEscort(nil, FollowGroup, self.escortVec3, nil, self.engageMaxDistance, self.engageTargetTypes)
|
||||
end
|
||||
|
||||
table.insert(DCStasks, DCStask)
|
||||
|
||||
|
||||
@@ -150,6 +150,9 @@ function BRIGADE:New(WarehouseName, BrigadeName)
|
||||
-- @param Ops.ArmyGroup#ARMYGROUP ArmyGroup The ARMYGROUP on mission.
|
||||
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
|
||||
|
||||
-- Add legion to DB
|
||||
_DATABASE:AddLegion(self)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
@@ -183,6 +183,7 @@ do
|
||||
-- my_ctld.placeCratesAhead = false -- place crates straight ahead of the helicopter, in a random way. If true, crates are more neatly sorted.
|
||||
-- my_ctld.nobuildinloadzones = true -- forbid players to build stuff in LOAD zones if set to `true`
|
||||
-- my_ctld.movecratesbeforebuild = true -- crates must be moved once before they can be build. Set to false for direct builds.
|
||||
-- my_ctld.useaircraftheadingforbuilds = false -- Matches the heading of the spawned groups from a crate build to the aircraft heading when starting the build. Default is false for template heading.
|
||||
-- my_ctld.surfacetypes = {land.SurfaceType.LAND,land.SurfaceType.ROAD,land.SurfaceType.RUNWAY,land.SurfaceType.SHALLOW_WATER} -- surfaces for loading back objects.
|
||||
-- my_ctld.nobuildmenu = false -- if set to true effectively enforces to have engineers build/repair stuff for you.
|
||||
-- my_ctld.RadioSound = "beacon.ogg" -- -- this sound will be hearable if you tune in the beacon frequency. Add the sound file to your miz.
|
||||
@@ -2037,7 +2038,7 @@ function CTLD:_C130DcAutoRegisterEntry(SetId, Cargo)
|
||||
|
||||
local pos = Cargo:GetPositionable()
|
||||
local pname = pos and pos.GetName and pos:GetName() or nil
|
||||
local pcoord = pos and pos.GetCoordinate and pos:GetCoordinate() or nil
|
||||
local pcoord = pos and pos.GetCoord and pos:GetCoord() or nil
|
||||
local entryId = string.format("%s#%d", SetId, #setData.entries + 1)
|
||||
local entry = {
|
||||
id = entryId,
|
||||
@@ -2748,16 +2749,14 @@ function CTLD:_EventHandler(EventData)
|
||||
loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo
|
||||
loaded.Cratesloaded = loaded.Cratesloaded - 1
|
||||
if loaded.Cratesloaded < 0 then loaded.Cratesloaded = 0 end
|
||||
-- TODO zap cargo from list
|
||||
local Loaded = {}
|
||||
for _,_item in pairs (loaded.Cargo or {}) do
|
||||
self:T(self.lid.."UNLOAD checking: ".._item:GetName())
|
||||
self:T(self.lid.."UNLOAD state: ".. tostring(_item:WasDropped()))
|
||||
if _item and _item:GetType() == CTLD_CARGO.Enum.GCLOADABLE and event.IniDynamicCargoName and event.IniDynamicCargoName ~= _item:GetName() and not _item:WasDropped() then
|
||||
local unloadedItem = _item and _item:GetType() == CTLD_CARGO.Enum.GCLOADABLE and event.IniDynamicCargoName and event.IniDynamicCargoName == _item:GetName()
|
||||
if not unloadedItem then
|
||||
table.insert(Loaded,_item)
|
||||
else
|
||||
table.insert(Loaded,_item)
|
||||
end
|
||||
end
|
||||
end
|
||||
loaded.Cargo = nil
|
||||
loaded.Cargo = Loaded
|
||||
@@ -3125,14 +3124,14 @@ end
|
||||
function CTLD:_FindRepairNearby(Group, Unit, Repairtype)
|
||||
self:T(self.lid .. " _FindRepairNearby")
|
||||
--self:T({Group:GetName(),Unit:GetName(),Repairtype})
|
||||
local unitcoord = Unit:GetCoordinate()
|
||||
local unitcoord = Unit:GetCoord()
|
||||
|
||||
-- find nearest group of deployed groups
|
||||
local nearestGroup = nil
|
||||
local nearestGroupIndex = -1
|
||||
local nearestDistance = 10000
|
||||
for k,v in pairs(self.DroppedTroops) do
|
||||
local distance = self:_GetDistance(v:GetCoordinate(),unitcoord)
|
||||
local distance = self:_GetDistance(v:GetCoord(),unitcoord)
|
||||
local unit = v:GetUnit(1) -- Wrapper.Unit#UNIT
|
||||
local desc = unit:GetDesc() or nil
|
||||
if distance < nearestDistance and distance ~= -1 and not desc.attributes.Infantry then
|
||||
@@ -3277,7 +3276,7 @@ end
|
||||
local capabilities = self:_GetUnitCapabilities(Unit)
|
||||
local cantroops = capabilities.troops -- #boolean
|
||||
local trooplimit = capabilities.trooplimit -- #number
|
||||
local unitcoord = unit:GetCoordinate()
|
||||
local unitcoord = unit:GetCoord()
|
||||
|
||||
-- find nearest group of deployed troops
|
||||
local nearestGroup = nil
|
||||
@@ -3288,7 +3287,7 @@ end
|
||||
local distancekeys = {}
|
||||
local extractdistance = self.CrateDistance * self.ExtractFactor
|
||||
for k,v in pairs(self.DroppedTroops) do
|
||||
local distance = self:_GetDistance(v:GetCoordinate(),unitcoord)
|
||||
local distance = self:_GetDistance(v:GetCoord(),unitcoord)
|
||||
local TNow = timer.getTime()
|
||||
local vtime = v.ExtractTime or TNow-310
|
||||
if distance <= extractdistance and distance ~= -1 and (TNow - vtime > 300) then
|
||||
@@ -3376,7 +3375,7 @@ end
|
||||
self:_UpdateUnitCargoMass(Unit)
|
||||
local groupname = nearestGroup:GetName()
|
||||
self:__TroopsExtracted(running,Group, Unit, nearestGroup, groupname)
|
||||
local coord = Unit:GetCoordinate() or Group:GetCoordinate() -- Core.Point#COORDINATE
|
||||
local coord = Unit:GetCoord() or Group:GetCoord() -- Core.Point#COORDINATE
|
||||
local Point
|
||||
if coord then
|
||||
local heading = unit:GetHeading() or 0
|
||||
@@ -3747,16 +3746,16 @@ function CTLD:_C130GetUnits(Group, Unit, Name)
|
||||
return self
|
||||
end
|
||||
|
||||
local coord = Unit:GetCoordinate() or Group:GetCoordinate()
|
||||
local coord = Unit:GetCoord() or Group:GetCoord()
|
||||
local capabilities = self:_GetUnitCapabilities(Unit)
|
||||
local innerDist = (capabilities.length and capabilities.length/2) or 15
|
||||
local maxUnitsNearby = self.maxUnitsNearby or 3
|
||||
local searchRadius = self.UnitDistance or 90
|
||||
local checkZone = ZONE_RADIUS:New("CTLD_C130UnitsZone",coord:GetVec2(),searchRadius,false)
|
||||
local nearGroups = SET_GROUP:New():FilterCoalitions("blue"):FilterZones({checkZone}):FilterOnce()
|
||||
local nearGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterZones({checkZone}):FilterOnce()
|
||||
local nearbyCount = 0
|
||||
for _,gr in pairs(nearGroups.Set) do
|
||||
local gc = gr:GetCoordinate()
|
||||
local gc = gr:GetCoord()
|
||||
if gc then
|
||||
local dist = coord:Get2DDistance(gc)
|
||||
if dist > innerDist then
|
||||
@@ -3806,6 +3805,7 @@ function CTLD:_C130GetUnits(Group, Unit, Name)
|
||||
if canmove then
|
||||
SPAWN:NewWithAlias(_template,alias)
|
||||
:InitRandomizeUnits(true,10,2)
|
||||
:InitCoalition(self.coalition)
|
||||
:InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits, 70)
|
||||
:InitDelayOff()
|
||||
:OnSpawnGroup(function(grp,TimeStamp)
|
||||
@@ -3819,6 +3819,7 @@ function CTLD:_C130GetUnits(Group, Unit, Name)
|
||||
SPAWN:NewWithAlias(_template,alias)
|
||||
:InitRandomizeUnits(true,10,2)
|
||||
:InitDelayOff()
|
||||
:InitCoalition(self.coalition)
|
||||
:InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits, 70)
|
||||
:OnSpawnGroup(function(grp,TimeStamp)
|
||||
grp.spawntime = TimeStamp or timer.getTime()
|
||||
@@ -3925,7 +3926,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
|
||||
-- Check cargo location if available
|
||||
local location = Cargo:GetLocation()
|
||||
if location then
|
||||
local unitcoord = Unit:GetCoordinate() or Group:GetCoordinate()
|
||||
local unitcoord = Unit:GetCoord() or Group:GetCoord()
|
||||
if unitcoord then
|
||||
if not location:IsCoordinateInZone(unitcoord) then
|
||||
-- no we're not at the right spot
|
||||
@@ -3972,7 +3973,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
|
||||
end
|
||||
|
||||
-- get position and heading of heli
|
||||
local position = Unit:GetCoordinate()
|
||||
local position = Unit:GetCoord()
|
||||
local heading = Unit:GetHeading() + 1
|
||||
local height = Unit:GetHeight()
|
||||
local droppedcargo = {}
|
||||
@@ -4136,8 +4137,8 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
|
||||
if type(ship) == "string" then
|
||||
self:T("Spawning on ship "..ship)
|
||||
local Ship = UNIT:FindByName(ship)
|
||||
local shipcoord = Ship:GetCoordinate()
|
||||
local unitcoord = Unit:GetCoordinate()
|
||||
local shipcoord = Ship:GetCoord()
|
||||
local unitcoord = Unit:GetCoord()
|
||||
local dist = shipcoord:Get2DDistance(unitcoord)
|
||||
dist = dist - (20 + math.random(1, 10))
|
||||
local halfwidth = (width or 20) / 2
|
||||
@@ -4389,17 +4390,17 @@ end
|
||||
function CTLD:_C130RemoveUnitsNearby(_group,_unit)
|
||||
self:T(self.lid .. " _C130RemoveUnitsNearby")
|
||||
if not _group or not _unit then return self end
|
||||
local location = _group:GetCoordinate()
|
||||
local location = _group:GetCoord()
|
||||
if not location then return self end
|
||||
local capabilities = self:_GetUnitCapabilities(_unit)
|
||||
local innerDist = (capabilities.length and capabilities.length/2) or 15
|
||||
local finddist = self.PackDistance or (self.CrateDistance or 35)
|
||||
local zone = ZONE_RADIUS:New("CTLD_C130RemoveZone",location:GetVec2(),finddist,false)
|
||||
local nearestGroups = SET_GROUP:New():FilterCoalitions("blue"):FilterZones({zone}):FilterOnce()
|
||||
local nearestGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterZones({zone}):FilterOnce()
|
||||
local removedAny = false
|
||||
local removedTable = {}
|
||||
for _, gr in pairs(nearestGroups.Set) do
|
||||
local gc = gr:GetCoordinate()
|
||||
local gc = gr:GetCoord()
|
||||
if gc then
|
||||
local dist = location:Get2DDistance(gc)
|
||||
if dist > innerDist then
|
||||
@@ -4533,7 +4534,7 @@ end
|
||||
function CTLD:_FindCratesNearby( _group, _unit, _dist, _ignoreweight, ignoretype, ignoreHercInner)
|
||||
self:T(self.lid .. " _FindCratesNearby")
|
||||
local finddist = _dist
|
||||
local location = _group:GetCoordinate()
|
||||
local location = _group:GetCoord()
|
||||
local existingcrates = self.Spawned_Cargo -- #table
|
||||
-- cycle
|
||||
local index = 0
|
||||
@@ -5159,7 +5160,6 @@ function CTLD:_UnloadTroops(Group, Unit)
|
||||
-- unload troops
|
||||
local name = cargo:GetName() or "none"
|
||||
local temptable = cargo:GetTemplates() or {}
|
||||
local position = Group:GetCoordinate()
|
||||
local zoneradius = self.troopdropzoneradius or 100 -- drop zone radius
|
||||
local factor = 1
|
||||
if IsHerc then
|
||||
@@ -5196,6 +5196,7 @@ function CTLD:_UnloadTroops(Group, Unit)
|
||||
local Positions = self:_GetUnitPositions(randomcoord,rad,heading,_template)
|
||||
self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
|
||||
:InitDelayOff()
|
||||
:InitCoalition(self.coalition)
|
||||
:InitSetUnitAbsolutePositions(Positions)
|
||||
:InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
|
||||
:OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end)
|
||||
@@ -5506,8 +5507,8 @@ function CTLD:_BuildCrates(Group, Unit,Engineering,MultiDrop,NotifyGroup)
|
||||
local required = Crate:GetCratesNeeded()
|
||||
local template = Crate:GetTemplates()
|
||||
local ctype = Crate:GetType()
|
||||
local ccoord = Crate:GetPositionable():GetCoordinate() -- Core.Point#COORDINATE
|
||||
local distToUnit=Unit and ccoord:Get2DDistance(Unit:GetCoordinate())or 0
|
||||
local ccoord = Crate:GetPositionable():GetCoord() -- Core.Point#COORDINATE
|
||||
local distToUnit=Unit and ccoord:Get2DDistance(Unit:GetCoord())or 0
|
||||
local isHercDrop=Crate:WasDropped(true)
|
||||
if not isHercDrop and distToUnit>baseDist then
|
||||
elseif self:IsC130J(Unit) and distToUnit<15 then
|
||||
@@ -5526,6 +5527,7 @@ function CTLD:_BuildCrates(Group, Unit,Engineering,MultiDrop,NotifyGroup)
|
||||
object.Found = 1
|
||||
object.Template = template
|
||||
object.CanBuild = false
|
||||
object.Heading = Unit:GetHeading()
|
||||
object.Type = ctype -- #CTLD_CARGO.Enum
|
||||
object.Coord = ccoord:GetVec2()
|
||||
buildables[name] = object
|
||||
@@ -5611,7 +5613,7 @@ function CTLD:_BuildCrates(Group, Unit,Engineering,MultiDrop,NotifyGroup)
|
||||
local sep = self.buildPairSeparation or 25
|
||||
local hdg = (Unit:GetHeading()+180)%360
|
||||
local lat = (hdg+90)%360
|
||||
local base = Unit:GetCoordinate():Translate(20,hdg)
|
||||
local base = Unit:GetCoord():Translate(20,hdg)
|
||||
|
||||
if full == 1 then
|
||||
local cratesNow, numberNow = self:_FindCratesNearby(Group,Unit, finddist,true,true, not Engineering)
|
||||
@@ -5645,7 +5647,7 @@ function CTLD:_BuildCrates(Group, Unit,Engineering,MultiDrop,NotifyGroup)
|
||||
self:_RefreshPackMenus(Group,Unit)
|
||||
local off = start + (n-1)*sep
|
||||
local coord = base:Translate(off,lat):GetVec2()
|
||||
local b = { Name=build.Name, Required=build.Required, Template=build.Template, CanBuild=true, Type=build.Type, Coord=coord }
|
||||
local b = { Name=build.Name, Required=build.Required, Template=build.Template, CanBuild=true, Type=build.Type, Coord=coord, Heading=build.Heading }
|
||||
if self.buildtime and self.buildtime > 0 then
|
||||
local buildtimer = TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,b,false,Group:GetCoordinate(),MultiDrop)
|
||||
buildtimer:Start(self.buildtime)
|
||||
@@ -5683,18 +5685,18 @@ end
|
||||
-- @return #number Number
|
||||
function CTLD:_FindPackableGroupsNearby(Group, Unit)
|
||||
self:T(self.lid .. " _FindPackableGroupsNearby")
|
||||
local location = Group:GetCoordinate()
|
||||
local location = Group:GetCoord()
|
||||
if not location then return {}, 0 end
|
||||
local capabilities = self:_GetUnitCapabilities(Unit)
|
||||
local innerDist = (capabilities.length and capabilities.length/2) or 15
|
||||
local finddist = self.PackDistance or (self.CrateDistance or 35)
|
||||
local zone = ZONE_RADIUS:New("CTLD_PackableZone", location:GetVec2(), finddist, false)
|
||||
local nearestGroups = SET_GROUP:New():FilterCoalitions("blue"):FilterZones({zone}):FilterOnce()
|
||||
local nearestGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterZones({zone}):FilterOnce()
|
||||
local packable = {}
|
||||
|
||||
for _, gr in pairs(nearestGroups.Set) do
|
||||
if gr and gr:GetName() ~= Group:GetName() then
|
||||
local gc = gr:GetCoordinate()
|
||||
local gc = gr:GetCoord()
|
||||
if gc then
|
||||
local dist = location:Get2DDistance(gc)
|
||||
if dist > innerDist and dist <= finddist then
|
||||
@@ -5827,12 +5829,12 @@ function CTLD:_LoadPackedCratesByIds(Group, Unit, crateIds, cargoName)
|
||||
|
||||
local matchingCrates = {}
|
||||
local finddist = self.CrateDistance or 35
|
||||
local location = Group:GetCoordinate()
|
||||
local location = Group:GetCoord()
|
||||
for _, crateObj in pairs(self.Spawned_Cargo or {}) do
|
||||
if crateObj and idLookup[crateObj:GetID()] then
|
||||
local pos = crateObj:GetPositionable()
|
||||
if pos and pos:IsAlive() then
|
||||
local dist = location:Get2DDistance(pos:GetCoordinate())
|
||||
local dist = location:Get2DDistance(pos:GetCoord())
|
||||
if dist <= finddist then
|
||||
matchingCrates[#matchingCrates + 1] = crateObj
|
||||
end
|
||||
@@ -5942,12 +5944,12 @@ function CTLD:_RemovePackedCratesByIds(Group, Unit, crateIds)
|
||||
|
||||
local crates = {}
|
||||
local finddist = self.CrateDistance or 35
|
||||
local location = Group:GetCoordinate()
|
||||
local location = Group:GetCoord()
|
||||
for _, entry in pairs(self.Spawned_Cargo or {}) do
|
||||
if entry and idLookup[entry:GetID()] then
|
||||
local pos = entry:GetPositionable()
|
||||
if pos and pos:IsAlive() then
|
||||
local dist = location:Get2DDistance(pos:GetCoordinate())
|
||||
local dist = location:Get2DDistance(pos:GetCoord())
|
||||
if dist <= finddist then
|
||||
crates[#crates + 1] = entry
|
||||
end
|
||||
@@ -6171,11 +6173,11 @@ function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation,Mult
|
||||
local ctype = Build.Type -- #CTLD_CARGO.Enum
|
||||
local canmove = false
|
||||
if ctype == CTLD_CARGO.Enum.VEHICLE then canmove = true end
|
||||
if ctype == CTLD_CARGO.Enum.STATIC then
|
||||
return self
|
||||
if ctype == CTLD_CARGO.Enum.STATIC then
|
||||
return self
|
||||
end
|
||||
local temptable = Build.Template or {}
|
||||
if type(temptable) == "string" then
|
||||
if type(temptable) == "string" then
|
||||
temptable = {temptable}
|
||||
end
|
||||
local zone = nil -- Core.Zone#ZONE_RADIUS
|
||||
@@ -6197,28 +6199,31 @@ function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation,Mult
|
||||
for _,_template in pairs(temptable) do
|
||||
self.TroopCounter = self.TroopCounter + 1
|
||||
local alias = string.format("%s-%d", _template, math.random(1,100000))
|
||||
local spawn = SPAWN:NewWithAlias(_template,alias)
|
||||
:InitDelayOff()
|
||||
:InitCoalition(self.coalition)
|
||||
:InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
|
||||
:OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end)
|
||||
if canmove then
|
||||
self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
|
||||
--:InitRandomizeUnits(true,20,2)
|
||||
:InitDelayOff()
|
||||
:InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
|
||||
:OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end)
|
||||
:SpawnFromVec2(randomcoord)
|
||||
else -- don't random position of e.g. SAM units build as FOB
|
||||
self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
|
||||
:InitDelayOff()
|
||||
:InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
|
||||
:OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end)
|
||||
:SpawnFromVec2(randomcoord)
|
||||
--spawn:InitRandomizeUnits(true,20,2)
|
||||
end
|
||||
if Build.Heading and self.useaircraftheadingforbuilds then
|
||||
spawn:InitGroupHeading(Build.Heading)
|
||||
end
|
||||
local spawnedGroup = spawn:SpawnFromVec2(randomcoord)
|
||||
self.DroppedTroops[self.TroopCounter] = spawnedGroup
|
||||
|
||||
if Repair then
|
||||
self:__CratesRepaired(1,Group,Unit,self.DroppedTroops[self.TroopCounter])
|
||||
self:__CratesRepaired(1,Group,Unit,spawnedGroup)
|
||||
else
|
||||
self:__CratesBuild(1,Group,Unit,self.DroppedTroops[self.TroopCounter])
|
||||
self:__CratesBuild(1,Group,Unit,spawnedGroup)
|
||||
end
|
||||
end -- template loop
|
||||
self:_RefreshLoadCratesMenu(Group, Unit)
|
||||
self:_RefreshPackMenus(Group, Unit)
|
||||
|
||||
if Group and Group:IsAlive() and Group:GetID() then
|
||||
self:_RefreshLoadCratesMenu(Group, Unit)
|
||||
self:_RefreshPackMenus(Group, Unit)
|
||||
end
|
||||
else
|
||||
self:T(self.lid.."Group KIA while building!")
|
||||
end
|
||||
@@ -6241,14 +6246,11 @@ end
|
||||
-- @param Wrapper.Group#GROUP Group The Group to move.
|
||||
function CTLD:_MoveGroupToZone(Group)
|
||||
self:T(self.lid .. " _MoveGroupToZone")
|
||||
local groupname = Group:GetName() or "none"
|
||||
local groupcoord = Group:GetCoordinate()
|
||||
-- Get closest zone of type
|
||||
local outcome, name, zone, distance = self:IsUnitInZone(Group,CTLD.CargoZoneType.MOVE)
|
||||
self:T({canmove=outcome, name=name, zone=zone, dist=distance,max=self.movetroopsdistance})
|
||||
if (distance <= self.movetroopsdistance) and outcome == true and zone~= nil then
|
||||
-- yes, we can ;)
|
||||
local groupname = Group:GetName()
|
||||
local zonecoord = zone:GetRandomCoordinate(20,125) -- Core.Point#COORDINATE
|
||||
local formation = self:_GetVehicleFormation()
|
||||
--local coordinate = zonecoord:GetVec2()
|
||||
@@ -7931,6 +7933,7 @@ function CTLD:_UnloadSingleTroopByID(Group, Unit, chunkID, qty)
|
||||
local Positions = self:_GetUnitPositions(randomcoord, rad, heading, _template)
|
||||
self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template, alias)
|
||||
:InitDelayOff()
|
||||
:InitCoalition(self.coalition)
|
||||
:InitSetUnitAbsolutePositions(Positions)
|
||||
:InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
|
||||
:OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end)
|
||||
@@ -9008,7 +9011,7 @@ function CTLD:IsUnitInZone(Unit,Zonetype)
|
||||
local zoneret = nil
|
||||
local zonewret = nil
|
||||
local zonenameret = nil
|
||||
local unitcoord = Unit:GetCoordinate()
|
||||
local unitcoord = Unit:GetCoord()
|
||||
if not unitcoord then
|
||||
if Zonetype == CTLD.CargoZoneType.SHIP then
|
||||
return false, nil, nil, 1000000, nil
|
||||
@@ -9029,7 +9032,7 @@ function CTLD:IsUnitInZone(Unit,Zonetype)
|
||||
self:T("Checking Type Ship: "..zonename)
|
||||
local ZoneUNIT = UNIT:FindByName(zonename)
|
||||
if not ZoneUNIT then return false end
|
||||
zonecoord = ZoneUNIT:GetCoordinate()
|
||||
zonecoord = ZoneUNIT:GetCoord()
|
||||
zoneradius = czone.shiplength
|
||||
zonewidth = czone.shipwidth
|
||||
zone = ZONE_UNIT:New( ZoneUNIT:GetName(), ZoneUNIT, zoneradius/2)
|
||||
@@ -9078,7 +9081,7 @@ function CTLD:SmokePositionNow(Unit, Flare, SmokeColor)
|
||||
end
|
||||
local FlareColor = self.FlareColor or FLARECOLOR.Red
|
||||
-- table of #CTLD.CargoZone table
|
||||
local unitcoord = Unit:GetCoordinate() -- Core.Point#COORDINATE
|
||||
local unitcoord = Unit:GetCoord() -- Core.Point#COORDINATE
|
||||
local Group = Unit:GetGroup()
|
||||
if Flare then
|
||||
unitcoord:Flare(FlareColor, 90)
|
||||
@@ -9097,7 +9100,7 @@ end
|
||||
function CTLD:SmokeZoneNearBy(Unit, Flare)
|
||||
self:T(self.lid .. " SmokeZoneNearBy")
|
||||
-- table of #CTLD.CargoZone table
|
||||
local unitcoord = Unit:GetCoordinate()
|
||||
local unitcoord = Unit:GetCoord()
|
||||
local Group = Unit:GetGroup()
|
||||
local smokedistance = self.smokedistance
|
||||
local smoked = false
|
||||
@@ -9234,7 +9237,7 @@ end
|
||||
-- get speed and height
|
||||
local uspeed = Unit:GetVelocityMPS()
|
||||
local uheight = Unit:GetHeight()
|
||||
local ucoord = Unit:GetCoordinate()
|
||||
local ucoord = Unit:GetCoord()
|
||||
if not ucoord then
|
||||
return false
|
||||
end
|
||||
@@ -9263,7 +9266,7 @@ end
|
||||
-- get speed and height
|
||||
local uspeed = Unit:GetVelocityMPS()
|
||||
local uheight = Unit:GetHeight()
|
||||
local ucoord = Unit:GetCoordinate()
|
||||
local ucoord = Unit:GetCoord()
|
||||
if not ucoord then
|
||||
return false
|
||||
end
|
||||
@@ -9355,7 +9358,7 @@ end
|
||||
minheight = 5.1 -- herc is 5m AGL on the ground
|
||||
end
|
||||
local uheight = Unit:GetHeight()
|
||||
local ucoord = Unit:GetCoordinate()
|
||||
local ucoord = Unit:GetCoord()
|
||||
if not ucoord then
|
||||
return false
|
||||
end
|
||||
@@ -10141,6 +10144,7 @@ end
|
||||
local alias = string.format("%s-%d", _template, math.random(1,100000))
|
||||
self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
|
||||
:InitRandomizeUnits(randompositions,20,2)
|
||||
:InitCoalition(self.coalition)
|
||||
:InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
|
||||
:InitDelayOff()
|
||||
:OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() end,TimeStamp)
|
||||
@@ -10295,6 +10299,7 @@ end
|
||||
if canmove then
|
||||
self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
|
||||
:InitRandomizeUnits(true,20,2)
|
||||
:InitCoalition(self.coalition)
|
||||
:InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
|
||||
:InitDelayOff()
|
||||
:OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() end,TimeStamp)
|
||||
@@ -10302,6 +10307,7 @@ end
|
||||
else -- don't random position of e.g. SAM units build as FOB
|
||||
self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
|
||||
:InitDelayOff()
|
||||
:InitCoalition(self.coalition)
|
||||
:InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
|
||||
:OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() end,TimeStamp)
|
||||
:SpawnFromVec2(randomcoord)
|
||||
@@ -10583,7 +10589,7 @@ end
|
||||
self:T({From, Event, To})
|
||||
if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then
|
||||
local playername = Unit:GetPlayerName()
|
||||
local dropcoord = Troops:GetCoordinate() or COORDINATE:New(0,0,0)
|
||||
local dropcoord = Troops:GetCoord() or COORDINATE:New(0,0,0)
|
||||
local dropvec2 = dropcoord:GetVec2()
|
||||
self.PlayerTaskQueue:ForEach(
|
||||
function (Task)
|
||||
@@ -10640,7 +10646,7 @@ end
|
||||
for _,_cargo in pairs(Cargotable) do
|
||||
local Vehicle = _cargo.Positionable
|
||||
if Vehicle then
|
||||
local dropcoord = Vehicle:GetCoordinate() or COORDINATE:New(0,0,0)
|
||||
local dropcoord = Vehicle:GetCoord() or COORDINATE:New(0,0,0)
|
||||
local dropvec2 = dropcoord:GetVec2()
|
||||
self.PlayerTaskQueue:ForEach(
|
||||
function (Task)
|
||||
@@ -10705,7 +10711,7 @@ end
|
||||
self:T({From, Event, To})
|
||||
if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then
|
||||
local playername = Unit:GetPlayerName()
|
||||
local dropcoord = Vehicle:GetCoordinate() or COORDINATE:New(0,0,0)
|
||||
local dropcoord = Vehicle:GetCoord() or COORDINATE:New(0,0,0)
|
||||
local dropvec2 = dropcoord:GetVec2()
|
||||
self.PlayerTaskQueue:ForEach(
|
||||
function (Task)
|
||||
|
||||
@@ -1102,6 +1102,31 @@ function COHORT:CountAssets(InStock, MissionTypes, Attributes)
|
||||
return N
|
||||
end
|
||||
|
||||
--- Count available assets in legion warehouse stock. Spawned, requested and reserved assets are not counted.
|
||||
-- @param #COHORT self
|
||||
-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types.
|
||||
-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`.
|
||||
-- @return #number Number of assets.
|
||||
function COHORT:CountAvailableAssets(MissionTypes, Attributes)
|
||||
|
||||
local N=0
|
||||
for _,_asset in pairs(self.assets) do
|
||||
local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
|
||||
|
||||
if not (asset.spawned or asset.requested or asset.isReserved) then
|
||||
|
||||
if MissionTypes==nil or AUFTRAG.CheckMissionCapability(MissionTypes, self.missiontypes) then
|
||||
if Attributes==nil or self:CheckAttribute(Attributes) then
|
||||
N=N+1 --This is in stock.
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return N
|
||||
end
|
||||
|
||||
|
||||
--- Get OPSGROUPs.
|
||||
-- @param #COHORT self
|
||||
-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types.
|
||||
|
||||
@@ -1882,6 +1882,8 @@ function COMMANDER:RecruitAssetsForMission(Mission)
|
||||
-- Recruite assets.
|
||||
local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, Mission.type, Mission.alert5MissionType, NreqMin, NreqMax, TargetVec2, Payloads,
|
||||
Mission.engageRange, Mission.refuelSystem, nil, nil, MaxWeight, nil, Mission.attributes, Mission.properties, {Mission.engageWeaponType})
|
||||
|
||||
self:T(self.lid..string.format("Recruited=%s Nassets=%d", tostring(recruited), #assets))
|
||||
|
||||
return recruited, assets, legions
|
||||
end
|
||||
@@ -2112,6 +2114,22 @@ function COMMANDER:CountAssets(InStock, MissionTypes, Attributes)
|
||||
return N
|
||||
end
|
||||
|
||||
--- Count available assets of all assigned legions, which are in stock. Spawned and reserved assets are not counted.
|
||||
-- @param #COMMANDER self
|
||||
-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types.
|
||||
-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`.
|
||||
-- @return #number Amount of asset groups.
|
||||
function COMMANDER:CountAvailableAssets(MissionTypes, Attributes)
|
||||
|
||||
local N=0
|
||||
for _,_legion in pairs(self.legions) do
|
||||
local legion=_legion --Ops.Legion#LEGION
|
||||
N=N+legion:CountAvailableAssets(MissionTypes, Attributes)
|
||||
end
|
||||
|
||||
return N
|
||||
end
|
||||
|
||||
--- Count assets of all assigned legions.
|
||||
-- @param #COMMANDER self
|
||||
-- @param #table MissionTypes (Optional) Count only missions of these types. Default is all types.
|
||||
|
||||
@@ -167,6 +167,9 @@ function FLEET:New(WarehouseName, FleetName)
|
||||
-- @param Ops.NavyGroup#NAVYGROUP NavyGroup The NAVYGROUP on mission.
|
||||
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
|
||||
|
||||
-- Add legion to DB
|
||||
_DATABASE:AddLegion(self)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
@@ -73,6 +73,9 @@ function FLOTILLA:New(TemplateGroupName, Ngroups, FlotillaName)
|
||||
-- Get initial ammo.
|
||||
self.ammo=self:_CheckAmmo()
|
||||
|
||||
-- Add cohort to DB
|
||||
_DATABASE:AddCohort(self)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
@@ -350,6 +350,18 @@ INTEL.RCS_NoseOnFraction = 0.15
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Constructor
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
--- Create a new INTEL object and start the FSM.
|
||||
-- @param #INTEL self
|
||||
-- @return #INTEL self
|
||||
function INTEL:_UpdateAgents()
|
||||
|
||||
-- Filter coalition.
|
||||
if self.coalition then
|
||||
local coalitionname=UTILS.GetCoalitionName(self.coalition):lower()
|
||||
self.detectionset:FilterCoalitions(coalitionname):FilterAlive():FilterOnce()
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
--- Create a new INTEL object and start the FSM.
|
||||
-- @param #INTEL self
|
||||
@@ -837,6 +849,18 @@ function INTEL:AddAgent(AgentGroup)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Set whether the detection set, aka agents, are updated periodically. With this all alive groups of the INTEL coalition are included.
|
||||
-- @param #INTEL self
|
||||
-- @param #boolean switch If `true` or nil, all groups of the coalition will be added automatically to the Agent set.
|
||||
-- @return #INTEL self
|
||||
function INTEL:SetAgentAuto(switch)
|
||||
if switch==nil then
|
||||
switch=true
|
||||
end
|
||||
self.update_detectionset=switch
|
||||
return self
|
||||
end
|
||||
|
||||
--- Enable or disable cluster analysis of detected targets.
|
||||
-- Targets will be grouped in coupled clusters.
|
||||
-- @param #INTEL self
|
||||
@@ -1025,6 +1049,10 @@ function INTEL:onafterStatus(From, Event, To)
|
||||
|
||||
-- FSM state.
|
||||
local fsmstate=self:GetState()
|
||||
|
||||
if self.update_detectionset then
|
||||
self:_UpdateAgents()
|
||||
end
|
||||
|
||||
-- Fresh arrays.
|
||||
self.ContactsLost={}
|
||||
|
||||
@@ -306,7 +306,6 @@ function LEGION:New(WarehouseName, LegionName)
|
||||
-- @param Ops.Cohort#COHORT Cohort The cohort the asset belongs to.
|
||||
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset that returned.
|
||||
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
@@ -2099,6 +2098,23 @@ function LEGION:CountAssets(InStock, MissionTypes, Attributes)
|
||||
return N
|
||||
end
|
||||
|
||||
--- Count total number of available assets in the legion stock.
|
||||
-- @param #LEGION self
|
||||
-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types.
|
||||
-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `GROUP.Attribute.AIR_BOMBER`.
|
||||
-- @return #number Amount of asset groups in stock.
|
||||
function LEGION:CountAvailableAssets(MissionTypes, Attributes)
|
||||
|
||||
local N=0
|
||||
|
||||
for _,_cohort in pairs(self.cohorts) do
|
||||
local cohort=_cohort --Ops.Cohort#COHORT
|
||||
N=N+cohort:CountAvailableAssets(MissionTypes,Attributes)
|
||||
end
|
||||
|
||||
return N
|
||||
end
|
||||
|
||||
--- Get OPSGROUPs that are spawned and alive.
|
||||
-- @param #LEGION self
|
||||
-- @param #table MissionTypes (Optional) Get only assest that can perform certain mission type(s). Default is all types.
|
||||
@@ -2661,7 +2677,7 @@ function LEGION._CohortCan(Cohort, MissionType, Categories, Attributes, Properti
|
||||
local RangeMax = RangeMax or 0
|
||||
local InRange=(RangeMax and math.max(RangeMax, Rmax) or Rmax) >= TargetDistance and TargetDistance > RangeMin
|
||||
|
||||
--env.info(string.format("Range TargetDist=%.1f Rmax=%.1f RangeMax=%.1f InRange=%s RangeMin=%s", TargetDistance, Rmax, RangeMax, tostring(InRange),tostring(RangeMin)))
|
||||
--env.info(string.format("Range TargetDist=%.1f Rmax=%.1f RangeMax=%.1f InRange=%s", TargetDistance, Rmax, RangeMax, tostring(InRange)))
|
||||
|
||||
return InRange
|
||||
end
|
||||
|
||||
@@ -4691,7 +4691,7 @@ function OPSGROUP:_UpdateTask(Task, Mission)
|
||||
self:T(self.lid..string.format("Zone %s captured ==> Task DONE!", zoneCurr:GetName()))
|
||||
|
||||
-- Task done.
|
||||
if Task.StayInZoneTime then
|
||||
if Task.StayInZoneTime and Task.StayInZoneTime>0 then
|
||||
local stay = Task.StayInZoneTime
|
||||
self:__TaskDone(stay,Task)
|
||||
else
|
||||
|
||||
@@ -71,6 +71,9 @@ function PLATOON:New(TemplateGroupName, Ngroups, PlatoonName)
|
||||
|
||||
-- Get ammo.
|
||||
self.ammo=self:_CheckAmmo()
|
||||
|
||||
-- Add cohort to DB
|
||||
_DATABASE:AddCohort(self)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
@@ -116,6 +116,9 @@ function SQUADRON:New(TemplateGroupName, Ngroups, SquadronName)
|
||||
|
||||
-- See COHORT class
|
||||
|
||||
-- Add cohort to DB
|
||||
_DATABASE:AddCohort(self)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ _TARGETID=0
|
||||
|
||||
--- TARGET class version.
|
||||
-- @field #string version
|
||||
TARGET.version="0.7.1"
|
||||
TARGET.version="0.8.0"
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- TODO list
|
||||
@@ -206,6 +206,9 @@ function TARGET:New(TargetObject)
|
||||
self:AddTransition("*", "Status", "*") -- Status update.
|
||||
self:AddTransition("*", "Stop", "Stopped") -- Stop FSM.
|
||||
|
||||
self:AddTransition("*", "ElementDestroyed", "*") -- A target element was destroyed.
|
||||
self:AddTransition("*", "ElementDead", "*") -- A target element is dead (destroyed or despawned).
|
||||
|
||||
self:AddTransition("*", "ObjectDamaged", "*") -- A target object was damaged.
|
||||
self:AddTransition("*", "ObjectDestroyed", "*") -- A target object was destroyed.
|
||||
self:AddTransition("*", "ObjectDead", "*") -- A target object is dead (destroyed or despawned).
|
||||
@@ -245,6 +248,19 @@ function TARGET:New(TargetObject)
|
||||
-- @param #number delay Delay in seconds.
|
||||
|
||||
|
||||
--- Triggers the FSM event "ElementDestroyed".
|
||||
-- @function [parent=#TARGET] ElementDestroyed
|
||||
-- @param #TARGET self
|
||||
-- @param #string ElementName Name of the element.
|
||||
-- @param #TARGET.Object Target Target object.
|
||||
|
||||
--- Triggers the FSM event "ElementDead".
|
||||
-- @function [parent=#TARGET] ElementDead
|
||||
-- @param #TARGET self
|
||||
-- @param #string ElementName Name of the element.
|
||||
-- @param #TARGET.Object Target Target object.
|
||||
|
||||
|
||||
--- Triggers the FSM event "ObjectDamaged".
|
||||
-- @function [parent=#TARGET] ObjectDamaged
|
||||
-- @param #TARGET self
|
||||
@@ -642,6 +658,19 @@ function TARGET:onafterStatus(From, Event, To)
|
||||
|
||||
-- FSM state.
|
||||
local fsmstate=self:GetState()
|
||||
|
||||
-- First we check any target has been destroyed and the dead/unitlost event was not fired
|
||||
for i,_target in pairs(self.targets) do
|
||||
local target=_target --#TARGET.Object
|
||||
local life=self:GetTargetLife(target)
|
||||
if life<1 and target.Status~=TARGET.ObjectStatus.DEAD then
|
||||
self:E(self.lid..string.format("FF life is zero but no object dead event fired ==> waiting for target object %s events!", tostring(target.Name)))
|
||||
-- We wait 60 seconds for the events to occur
|
||||
self:__Status(-60)
|
||||
return self
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-- Update damage.
|
||||
local damaged=false
|
||||
@@ -736,6 +765,61 @@ end
|
||||
-- FSM Events
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- On after "ElementDestroyed" event.
|
||||
-- @param #TARGET self
|
||||
-- @param #string From From state.
|
||||
-- @param #string Event Event.
|
||||
-- @param #string To To state.
|
||||
-- @param #string Name Name of the element.
|
||||
-- @param #TARGET.Object Target Target object.
|
||||
function TARGET:onafterElementDestroyed(From, Event, To, Name, Target)
|
||||
-- Debug message.
|
||||
self:T(self.lid..string.format("Element %s of target object %s destroyed", Name, Target.Name))
|
||||
|
||||
-- Increase destroyed counter.
|
||||
Target.Ndestroyed=Target.Ndestroyed+1
|
||||
|
||||
-- Increase dead counter.
|
||||
self.Ndestroyed=self.Ndestroyed+1
|
||||
|
||||
self:ElementDead(Name, Target)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
--- On after "ElementDead" event.
|
||||
-- @param #TARGET self
|
||||
-- @param #string From From state.
|
||||
-- @param #string Event Event.
|
||||
-- @param #string To To state.
|
||||
-- @param #string Name Name of the element.
|
||||
-- @param #TARGET.Object Target Target object.
|
||||
function TARGET:onafterElementDead(From, Event, To, Name, Target)
|
||||
-- Debug message.
|
||||
self:T(self.lid..string.format("Element %s of target object %s dead", Name, Target.Name))
|
||||
|
||||
-- Increase dead counter.
|
||||
Target.Ndead=Target.Ndead+1
|
||||
|
||||
-- Increase dead counter.
|
||||
self.Ndead=self.Ndead+1
|
||||
|
||||
-- All dead ==> Trigger event.
|
||||
if Target.Ndestroyed==Target.N0 then
|
||||
|
||||
self:ObjectDestroyed(Target)
|
||||
|
||||
elseif Target.Ndead==Target.N0 then
|
||||
|
||||
self:ObjectDead(Target)
|
||||
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
--- On after "ObjectDamaged" event.
|
||||
-- @param #TARGET self
|
||||
-- @param #string From From state.
|
||||
@@ -761,11 +845,6 @@ function TARGET:onafterObjectDestroyed(From, Event, To, Target)
|
||||
-- Debug message.
|
||||
self:T(self.lid..string.format("Object %s destroyed", Target.Name))
|
||||
|
||||
-- Increase destroyed counter.
|
||||
self.Ndestroyed=self.Ndestroyed+1
|
||||
|
||||
Target.Ndestroyed=Target.Ndestroyed+1
|
||||
|
||||
Target.Life=0
|
||||
|
||||
-- Call object dead event.
|
||||
@@ -787,16 +866,10 @@ function TARGET:onafterObjectDead(From, Event, To, Target)
|
||||
|
||||
-- Set target status.
|
||||
Target.Status=TARGET.ObjectStatus.DEAD
|
||||
|
||||
-- Increase dead object counter
|
||||
Target.Ndead=Target.Ndead+1
|
||||
|
||||
|
||||
-- Set target object life to 0.
|
||||
Target.Life=0
|
||||
|
||||
-- Increase dead counter.
|
||||
self.Ndead=self.Ndead+1
|
||||
|
||||
|
||||
-- Check if anyone is alive?
|
||||
local dead=true
|
||||
for _,_target in pairs(self.targets) do
|
||||
@@ -889,54 +962,21 @@ function TARGET:OnEventUnitDeadOrLost(EventData)
|
||||
-- Add to the list of casualties.
|
||||
table.insert(self.casualties, Name)
|
||||
|
||||
-- Try to get target Group.
|
||||
local target=self:GetTargetByName(EventData.IniGroupName)
|
||||
|
||||
-- Try unit target.
|
||||
if not target then
|
||||
target=self:GetTargetByName(EventData.IniUnitName)
|
||||
end
|
||||
-- Get target from Group or Unit.
|
||||
local target=self:GetTargetByName(EventData.IniGroupName) or self:GetTargetByName(EventData.IniUnitName)
|
||||
|
||||
-- Check if we could find a target object.
|
||||
if target then
|
||||
|
||||
local Ndead=target.Ndead
|
||||
local Ndestroyed=target.Ndestroyed
|
||||
-- Increase dead/destroyed counter
|
||||
if EventData.id==EVENTS.RemoveUnit then
|
||||
Ndead=Ndead+1
|
||||
self:ElementDead(Name, target)
|
||||
else
|
||||
Ndestroyed=Ndestroyed+1
|
||||
Ndead=Ndead+1
|
||||
end
|
||||
|
||||
|
||||
-- Check if ALL objects are dead
|
||||
if Ndead==target.N0 then
|
||||
|
||||
if Ndestroyed>=target.N0 then
|
||||
|
||||
-- Debug message.
|
||||
self:T2(self.lid..string.format("EVENT ID=%d: target %s dead/lost ==> destroyed", EventData.id, tostring(target.Name)))
|
||||
|
||||
target.Life = 0
|
||||
|
||||
-- Trigger object destroyed event. This sets the Life to zero and increases Ndestroyed
|
||||
self:ObjectDestroyed(target)
|
||||
|
||||
else
|
||||
|
||||
-- Debug message.
|
||||
self:T2(self.lid..string.format("EVENT ID=%d: target %s removed ==> dead", EventData.id, tostring(target.Name)))
|
||||
|
||||
target.Life = 0
|
||||
|
||||
-- Trigger object dead event. This sets the Life to zero and increases Ndead counter
|
||||
self:ObjectDead(target)
|
||||
|
||||
end
|
||||
|
||||
self:ElementDestroyed(Name, target)
|
||||
end
|
||||
|
||||
else
|
||||
self:E(self.lid..string.format("ERROR: Could not get target from IniGroup or IniUnit name when event Dead or UnitLost occured! Stats are not correctly updated :("))
|
||||
end -- Event belongs to this TARGET
|
||||
|
||||
end
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,240 @@
|
||||
-- Optional AUFTRAG tracing and diagnostic extension for MOOSE Bridge.
|
||||
--
|
||||
-- Load after MooseBridge.lua and, when used together with execution commands,
|
||||
-- after MooseBridgeAuftragExecutionExtension.lua. This file only adds read-only
|
||||
-- tracing helpers and does not change AUFTRAG execution semantics.
|
||||
|
||||
if not MOOSE_BRIDGE then error("Load MooseBridge.lua before MooseBridgeAuftragTraceExtension.lua") end
|
||||
|
||||
local function trace_safe_tostring(value)
|
||||
if value == nil then return "nil" end
|
||||
return tostring(value)
|
||||
end
|
||||
|
||||
local function trace_split_object_id(object_id)
|
||||
if type(object_id) ~= "string" then return nil, nil end
|
||||
local prefix, name = string.match(object_id, "^([^:]+):(.+)$")
|
||||
if not prefix or not name then return nil, nil end
|
||||
return string.upper(prefix), name
|
||||
end
|
||||
|
||||
local function trace_append_unique(result, seen, value)
|
||||
if value == nil then return end
|
||||
local text = tostring(value)
|
||||
if text == "" or seen[text] then return end
|
||||
result[#result + 1] = text
|
||||
seen[text] = true
|
||||
end
|
||||
|
||||
local function trace_bool(value)
|
||||
if value == nil then return false end
|
||||
return value and true or false
|
||||
end
|
||||
|
||||
local function trace_table_count(value)
|
||||
if type(value) ~= "table" then return 0 end
|
||||
local count = 0
|
||||
for _, _ in pairs(value) do count = count + 1 end
|
||||
return count
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceAuftragId(value)
|
||||
if type(value) ~= "table" then return nil end
|
||||
return self:_AuftragObjectId(value)
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceCollectAuftragIds(queue)
|
||||
local result = {}; local seen = {}
|
||||
if type(queue) ~= "table" then return result end
|
||||
for _, auftrag in pairs(queue) do
|
||||
trace_append_unique(result, seen, self:_TraceAuftragId(auftrag))
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceQueueContainsAuftrag(queue, auftrag_id)
|
||||
if type(queue) ~= "table" then return false end
|
||||
for _, auftrag in pairs(queue) do
|
||||
if self:_TraceAuftragId(auftrag) == auftrag_id then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceFindAuftrag(auftrag_id)
|
||||
if type(auftrag_id) ~= "string" or auftrag_id == "" then return nil, nil end
|
||||
|
||||
if type(self.TrackedAuftraege) == "table" and type(self.TrackedAuftraege[auftrag_id]) == "table" then
|
||||
return self.TrackedAuftraege[auftrag_id], "bridge.tracked"
|
||||
end
|
||||
|
||||
if _DATABASE and type(_DATABASE.LEGIONS) == "table" then
|
||||
for _, legion in pairs(_DATABASE.LEGIONS) do
|
||||
local queues = {legion.missionqueue, legion.missions, legion.auftraege, legion.missionQueue}
|
||||
for _, queue in ipairs(queues) do
|
||||
if type(queue) == "table" then
|
||||
for _, auftrag in pairs(queue) do
|
||||
if self:_TraceAuftragId(auftrag) == auftrag_id then return auftrag, "legion.queue" end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- MOOSE stores all OPSGROUP specializations here despite the FLIGHTGROUPS name.
|
||||
if _DATABASE and type(_DATABASE.FLIGHTGROUPS) == "table" then
|
||||
for _, opsgroup in pairs(_DATABASE.FLIGHTGROUPS) do
|
||||
local current = opsgroup.currentmission or opsgroup.missioncurrent or opsgroup.currentMission
|
||||
if self:_TraceAuftragId(current) == auftrag_id then return current, "opsgroup.current" end
|
||||
local queues = {opsgroup.missionqueue, opsgroup.missions, opsgroup.auftraege, opsgroup.missionQueue}
|
||||
for _, queue in ipairs(queues) do
|
||||
if type(queue) == "table" then
|
||||
for _, auftrag in pairs(queue) do
|
||||
if self:_TraceAuftragId(auftrag) == auftrag_id then return auftrag, "opsgroup.queue" end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceLegionItem(legion_name, legion, auftrag_id, source)
|
||||
local item = self:_BuildLegionSnapshotItem(legion_name, legion, source)
|
||||
if type(item) ~= "table" then return nil end
|
||||
item.missionqueue_count = trace_table_count(legion and legion.missionqueue)
|
||||
item.missionqueue_contains_auftrag = self:_TraceQueueContainsAuftrag(legion and legion.missionqueue, auftrag_id)
|
||||
item.is_running = tostring(item.state or "") == "Running"
|
||||
return item
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceCohortItem(cohort_name, cohort, source)
|
||||
local item = self:_BuildCohortSnapshotItem(cohort_name, cohort, source)
|
||||
if type(item) ~= "table" then return nil end
|
||||
item.asset_count = item.asset_count or trace_table_count(cohort and cohort.assets)
|
||||
item.stock_asset_count = item.stock_asset_count or trace_table_count(cohort and cohort.stock)
|
||||
item.spawned_asset_count = item.spawned_asset_count or trace_table_count(cohort and cohort.spawnedassets)
|
||||
item.opsgroup_count = item.opsgroup_count or trace_table_count(cohort and cohort.opsgroups)
|
||||
return item
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceOpsGroupItem(opsgroup_name, opsgroup, auftrag_id, source)
|
||||
local item = self:_BuildOpsGroupSnapshotItem(opsgroup_name, opsgroup, source)
|
||||
if type(item) ~= "table" then return nil end
|
||||
item.current_contains_auftrag = item.auftrag_current_id == auftrag_id
|
||||
item.queue_contains_auftrag = self:_TraceQueueContainsAuftrag(opsgroup and opsgroup.missionqueue, auftrag_id)
|
||||
item.missionqueue_count = trace_table_count(opsgroup and opsgroup.missionqueue)
|
||||
return item
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceCollectLegions(auftrag_id)
|
||||
local result = {}
|
||||
if _DATABASE and type(_DATABASE.LEGIONS) == "table" then
|
||||
for name, legion in pairs(_DATABASE.LEGIONS) do
|
||||
local ok, item = pcall(function() return self:_TraceLegionItem(name, legion, auftrag_id, "database.LEGIONS") end)
|
||||
if ok and item then result[#result + 1] = item end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceCollectCohorts()
|
||||
local result = {}; local seen = {}
|
||||
if _DATABASE and type(_DATABASE.LEGIONS) == "table" then
|
||||
for _, legion in pairs(_DATABASE.LEGIONS) do
|
||||
if type(legion.cohorts) == "table" then
|
||||
for name, cohort in pairs(legion.cohorts) do
|
||||
local ok, item = pcall(function() return self:_TraceCohortItem(name, cohort, "legion.cohorts") end)
|
||||
if ok and item and item.object_id and not seen[item.object_id] then
|
||||
result[#result + 1] = item
|
||||
seen[item.object_id] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceCollectOpsGroups(auftrag_id)
|
||||
local result = {}; local seen = {}
|
||||
for name, opsgroup in pairs(self.RegisteredOpsGroups or {}) do
|
||||
local ok, item = pcall(function() return self:_TraceOpsGroupItem(name, opsgroup, auftrag_id, "registered") end)
|
||||
if ok and item and item.object_id and not seen[item.object_id] then
|
||||
result[#result + 1] = item
|
||||
seen[item.object_id] = true
|
||||
end
|
||||
end
|
||||
-- MOOSE stores all OPSGROUP specializations here despite the FLIGHTGROUPS name.
|
||||
if _DATABASE and type(_DATABASE.FLIGHTGROUPS) == "table" then
|
||||
for name, opsgroup in pairs(_DATABASE.FLIGHTGROUPS) do
|
||||
local ok, item = pcall(function() return self:_TraceOpsGroupItem(name, opsgroup, auftrag_id, "database.FLIGHTGROUPS") end)
|
||||
if ok and item and item.object_id and not seen[item.object_id] then
|
||||
result[#result + 1] = item
|
||||
seen[item.object_id] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_TraceBuild(auftrag_id)
|
||||
local auftrag, source = self:_TraceFindAuftrag(auftrag_id)
|
||||
local auftrag_item = nil
|
||||
if type(auftrag) == "table" then
|
||||
local ok, item = pcall(function() return self:_BuildAuftragSnapshotItem(auftrag, source or "trace") end)
|
||||
if ok then auftrag_item = item end
|
||||
end
|
||||
|
||||
local legions = self:_TraceCollectLegions(auftrag_id)
|
||||
local cohorts = self:_TraceCollectCohorts()
|
||||
local opsgroups = self:_TraceCollectOpsGroups(auftrag_id)
|
||||
|
||||
local matching_legions = {}
|
||||
for _, legion in ipairs(legions) do
|
||||
if legion.missionqueue_contains_auftrag then matching_legions[#matching_legions + 1] = legion.object_id end
|
||||
end
|
||||
|
||||
local matching_opsgroups = {}
|
||||
for _, opsgroup in ipairs(opsgroups) do
|
||||
if opsgroup.current_contains_auftrag or opsgroup.queue_contains_auftrag then matching_opsgroups[#matching_opsgroups + 1] = opsgroup.object_id end
|
||||
end
|
||||
|
||||
return {
|
||||
action="auftrag.trace",
|
||||
auftrag_id=auftrag_id,
|
||||
found=auftrag_item ~= nil,
|
||||
source=source,
|
||||
auftrag=auftrag_item,
|
||||
legions=legions,
|
||||
cohorts=cohorts,
|
||||
opsgroups=opsgroups,
|
||||
matching_legion_ids=matching_legions,
|
||||
matching_opsgroup_ids=matching_opsgroups,
|
||||
counts={
|
||||
legions=#legions,
|
||||
cohorts=#cohorts,
|
||||
opsgroups=#opsgroups,
|
||||
matching_legions=#matching_legions,
|
||||
matching_opsgroups=#matching_opsgroups,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:RegisterAuftragTraceCommands()
|
||||
self:RegisterCommand("auftrag.trace", function(cmd)
|
||||
local p = self:_CommandParams(cmd)
|
||||
local auftrag_id = p.auftrag_id or p.object_id or p.id
|
||||
if type(auftrag_id) ~= "string" or auftrag_id == "" then error("auftrag.trace requires auftrag_id") end
|
||||
local prefix, _ = trace_split_object_id(auftrag_id)
|
||||
if prefix ~= "AUFTRAG" then error("auftrag.trace requires an AUFTRAG:<id> object id") end
|
||||
return self:_TraceBuild(auftrag_id)
|
||||
end)
|
||||
end
|
||||
|
||||
local _moose_bridge_base_register_default_commands_for_trace = MOOSE_BRIDGE.RegisterDefaultCommands
|
||||
|
||||
function MOOSE_BRIDGE:RegisterDefaultCommands()
|
||||
_moose_bridge_base_register_default_commands_for_trace(self)
|
||||
self:RegisterAuftragTraceCommands()
|
||||
end
|
||||
@@ -0,0 +1,297 @@
|
||||
--- DCS world event forwarding for MOOSE_BRIDGE.
|
||||
--
|
||||
-- Load after MooseBridge.lua and before constructing/starting the bridge.
|
||||
-- DCS events are normalized here; Python never needs to understand the raw
|
||||
-- world event table or MOOSE EVENTDATA implementation details.
|
||||
|
||||
if not MOOSE_BRIDGE then error("Load MooseBridge.lua before MooseBridgeDcsEventsExtension.lua") end
|
||||
|
||||
local function bridge_event_available(event_id)
|
||||
return type(event_id) == "number" and event_id > 0
|
||||
end
|
||||
|
||||
--- Cache current DCS ownership for all known MOOSE AIRBASE objects.
|
||||
-- The DCS BaseCaptured event already exposes the new owner, so the cache is
|
||||
-- needed to include the previous owner in the normalized bridge event.
|
||||
function MOOSE_BRIDGE:_CacheAirbaseCoalitions()
|
||||
self.AirbaseCoalitions = self.AirbaseCoalitions or {}
|
||||
if not _DATABASE or type(_DATABASE.AIRBASES) ~= "table" then return self end
|
||||
for airbase_name, airbase in pairs(_DATABASE.AIRBASES) do
|
||||
local ok, result = pcall(function()
|
||||
local name = self:_SafeCall(airbase, "GetName") or airbase.AirbaseName or airbase_name
|
||||
local owner = self:_CoalitionToName(self:_SafeCall(airbase, "GetCoalition"))
|
||||
return name and {object_id="AIRBASE:" .. tostring(name), coalition=owner} or nil
|
||||
end)
|
||||
if ok and result and result.object_id then
|
||||
self.AirbaseCoalitions[result.object_id] = result.coalition
|
||||
elseif not ok then
|
||||
self:_Log("Failed to cache airbase " .. tostring(airbase_name) .. ": " .. tostring(result))
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Resolve the authoritative MOOSE AIRBASE wrapper from an EVENTDATA object.
|
||||
function MOOSE_BRIDGE:_AirbaseFromCapturedEvent(EventData)
|
||||
if type(EventData) ~= "table" then return nil, nil end
|
||||
local place = EventData.Place
|
||||
local name = EventData.PlaceName
|
||||
if not name and place then
|
||||
name = self:_SafeCall(place, "GetName")
|
||||
end
|
||||
if not name and EventData.place then
|
||||
local ok, value = pcall(function() return EventData.place:getName() end)
|
||||
if ok then name = value end
|
||||
end
|
||||
if not name then return nil, nil end
|
||||
|
||||
local airbase = type(place) == "table" and place or nil
|
||||
if _DATABASE and type(_DATABASE.AIRBASES) == "table" then
|
||||
airbase = _DATABASE.AIRBASES[name] or airbase
|
||||
end
|
||||
if not airbase and AIRBASE and AIRBASE.FindByName then
|
||||
local ok, value = pcall(function() return AIRBASE:FindByName(name) end)
|
||||
if ok then airbase = value end
|
||||
end
|
||||
return airbase, tostring(name)
|
||||
end
|
||||
|
||||
--- Subscribe to selected low-frequency DCS events through MOOSE.
|
||||
function MOOSE_BRIDGE:_StartDcsEventForwarding()
|
||||
if self.DcsEventForwardingStarted then return self end
|
||||
if not EVENTS or not self.HandleEvent then
|
||||
self:_Log("DCS event forwarding unavailable")
|
||||
return self
|
||||
end
|
||||
self.DcsRegisteredEvents = {}
|
||||
if bridge_event_available(EVENTS.BaseCaptured) then
|
||||
self:_CacheAirbaseCoalitions()
|
||||
self:HandleEvent(EVENTS.BaseCaptured)
|
||||
self.DcsRegisteredEvents[#self.DcsRegisteredEvents + 1] = EVENTS.BaseCaptured
|
||||
self:_Log("DCS BaseCaptured event forwarding enabled")
|
||||
end
|
||||
if bridge_event_available(EVENTS.UnitLost) then
|
||||
self:HandleEvent(EVENTS.UnitLost)
|
||||
self.DcsRegisteredEvents[#self.DcsRegisteredEvents + 1] = EVENTS.UnitLost
|
||||
self:_Log("DCS UnitLost event forwarding enabled")
|
||||
end
|
||||
if bridge_event_available(EVENTS.Dead) then
|
||||
self:HandleEvent(EVENTS.Dead)
|
||||
self.DcsRegisteredEvents[#self.DcsRegisteredEvents + 1] = EVENTS.Dead
|
||||
self:_Log("DCS Dead event forwarding enabled")
|
||||
end
|
||||
if bridge_event_available(EVENTS.Kill) then
|
||||
self:HandleEvent(EVENTS.Kill)
|
||||
self.DcsRegisteredEvents[#self.DcsRegisteredEvents + 1] = EVENTS.Kill
|
||||
self:_Log("DCS Kill event forwarding enabled")
|
||||
end
|
||||
if bridge_event_available(EVENTS.MissionEnd) then
|
||||
self:HandleEvent(EVENTS.MissionEnd)
|
||||
self.DcsRegisteredEvents[#self.DcsRegisteredEvents + 1] = EVENTS.MissionEnd
|
||||
self:_Log("DCS MissionEnd event forwarding enabled")
|
||||
end
|
||||
if #self.DcsRegisteredEvents == 0 then
|
||||
self:_Log("No supported DCS events available for forwarding")
|
||||
return self
|
||||
end
|
||||
self.DcsEventForwardingStarted = true
|
||||
return self
|
||||
end
|
||||
|
||||
--- Unsubscribe from DCS events owned by this bridge instance.
|
||||
function MOOSE_BRIDGE:_StopDcsEventForwarding()
|
||||
if self.DcsEventForwardingStarted and self.UnHandleEvent then
|
||||
for _, event_id in ipairs(self.DcsRegisteredEvents or {}) do
|
||||
self:UnHandleEvent(event_id)
|
||||
end
|
||||
end
|
||||
self.DcsRegisteredEvents = {}
|
||||
self.DcsEventForwardingStarted = false
|
||||
return self
|
||||
end
|
||||
|
||||
--- Forward DCS S_EVENT_BASE_CAPTURED as airbase.coalition_changed.
|
||||
-- @param Core.Event#EVENTDATA EventData MOOSE-normalized DCS event data.
|
||||
function MOOSE_BRIDGE:OnEventBaseCaptured(EventData)
|
||||
local ok, err = pcall(function()
|
||||
local airbase, airbase_name = self:_AirbaseFromCapturedEvent(EventData)
|
||||
if not airbase or not airbase_name then
|
||||
error("BaseCaptured event has no resolvable AIRBASE")
|
||||
end
|
||||
|
||||
local item = self:_BuildAirbaseSnapshotItem(airbase_name, airbase)
|
||||
if not item or not item.object_id then
|
||||
error("Could not build AIRBASE snapshot for " .. tostring(airbase_name))
|
||||
end
|
||||
|
||||
self.AirbaseCoalitions = self.AirbaseCoalitions or {}
|
||||
local previous = self.AirbaseCoalitions[item.object_id]
|
||||
local current = item.coalition
|
||||
self.AirbaseCoalitions[item.object_id] = current
|
||||
|
||||
if previous ~= current then
|
||||
self:SendEvent("airbase.coalition_changed", {
|
||||
dcs_event_id=EventData.id,
|
||||
dcs_event_name="S_EVENT_BASE_CAPTURED",
|
||||
dcs_event_time=EventData.time,
|
||||
airbase_id=item.object_id,
|
||||
previous_coalition=previous,
|
||||
coalition=current,
|
||||
capturing_unit_id=EventData.IniUnitName and ("UNIT:" .. tostring(EventData.IniUnitName)) or nil,
|
||||
capturing_group_id=EventData.IniGroupName and ("GROUP:" .. tostring(EventData.IniGroupName)) or nil,
|
||||
capturing_coalition=self:_CoalitionToName(EventData.IniCoalition),
|
||||
capturing_unit_type=EventData.IniTypeName and tostring(EventData.IniTypeName) or nil,
|
||||
airbase=item,
|
||||
})
|
||||
end
|
||||
end)
|
||||
if not ok then
|
||||
self:_Log("Failed to forward BaseCaptured event: " .. tostring(err))
|
||||
end
|
||||
end
|
||||
|
||||
--- Build a tombstone and current group snapshot for a lost DCS object.
|
||||
function MOOSE_BRIDGE:_BuildUnitLostPayload(EventData)
|
||||
if type(EventData) ~= "table" then error("UnitLost event data is missing") end
|
||||
local name = EventData.IniUnitName or EventData.IniDCSUnitName
|
||||
if not name then error("UnitLost event has no initiator name") end
|
||||
|
||||
local is_static = Object and Object.Category
|
||||
and EventData.IniObjectCategory == Object.Category.STATIC
|
||||
local object_type = is_static and "STATIC" or "UNIT"
|
||||
local object_id = object_type .. ":" .. tostring(name)
|
||||
local item = nil
|
||||
|
||||
if is_static then
|
||||
local static = EventData.IniUnit
|
||||
if not static and _DATABASE and _DATABASE.STATICS then static = _DATABASE.STATICS[name] end
|
||||
if static then
|
||||
local ok, value = pcall(function() return self:_BuildStaticSnapshotItem(name, static) end)
|
||||
if ok then item = value end
|
||||
end
|
||||
else
|
||||
local unit = EventData.IniUnit
|
||||
if not unit and _DATABASE and _DATABASE.UNITS then unit = _DATABASE.UNITS[name] end
|
||||
if unit then
|
||||
local ok, value = pcall(function() return self:_BuildUnitSnapshotItem(name, unit) end)
|
||||
if ok then item = value end
|
||||
end
|
||||
end
|
||||
|
||||
item = item or {
|
||||
object_id=object_id,
|
||||
dcs_name=tostring(name),
|
||||
object_type=object_type,
|
||||
}
|
||||
item.object_id = object_id
|
||||
item.object_type = object_type
|
||||
item.alive = false
|
||||
item.active = false
|
||||
item.coalition = item.coalition or self:_CoalitionToName(EventData.IniCoalition)
|
||||
item.category = item.category or (EventData.IniCategory and tostring(EventData.IniCategory) or nil)
|
||||
item.dcs_type = item.dcs_type or (EventData.IniTypeName and tostring(EventData.IniTypeName) or nil)
|
||||
|
||||
local group_name = EventData.IniGroupName or EventData.IniDCSGroupName or item.group_name
|
||||
local group_item = nil
|
||||
if not is_static and group_name then
|
||||
local group = EventData.IniGroup
|
||||
if not group and _DATABASE and _DATABASE.GROUPS then group = _DATABASE.GROUPS[group_name] end
|
||||
if group then
|
||||
local ok, value = pcall(function() return self:_BuildGroupSnapshotItem(group_name, group) end)
|
||||
if ok then group_item = value end
|
||||
end
|
||||
item.group_name = tostring(group_name)
|
||||
end
|
||||
|
||||
return {
|
||||
object_id=object_id,
|
||||
object_type=object_type,
|
||||
group_id=group_name and ("GROUP:" .. tostring(group_name)) or nil,
|
||||
object=item,
|
||||
group=group_item,
|
||||
}
|
||||
end
|
||||
|
||||
--- Forward one DCS destruction event as object.destroyed.
|
||||
-- UnitLost and Dead can describe the same loss, depending on the DCS object
|
||||
-- and destruction path. Suppress the second event without polling object state.
|
||||
function MOOSE_BRIDGE:_ForwardObjectDestroyed(EventData, event_name)
|
||||
local ok, err = pcall(function()
|
||||
local payload = self:_BuildUnitLostPayload(EventData)
|
||||
local event_time = tonumber(EventData.time)
|
||||
local dedup_time = event_time or (timer and timer.getTime and timer.getTime()) or 0
|
||||
self.DcsDestroyedEventTimes = self.DcsDestroyedEventTimes or {}
|
||||
local previous_time = self.DcsDestroyedEventTimes[payload.object_id]
|
||||
if previous_time and math.abs(dedup_time - previous_time) <= 2 then return end
|
||||
self.DcsDestroyedEventTimes[payload.object_id] = dedup_time
|
||||
|
||||
payload.dcs_event_id = EventData.id
|
||||
payload.dcs_event_name = event_name
|
||||
payload.dcs_event_time = event_time
|
||||
self:SendEvent("object.destroyed", payload)
|
||||
end)
|
||||
if not ok then
|
||||
self:_Log("Failed to forward " .. tostring(event_name) .. " event: " .. tostring(err))
|
||||
end
|
||||
end
|
||||
|
||||
--- Forward DCS S_EVENT_UNIT_LOST as object.destroyed.
|
||||
-- @param Core.Event#EVENTDATA EventData MOOSE-normalized DCS event data.
|
||||
function MOOSE_BRIDGE:OnEventUnitLost(EventData)
|
||||
self:_ForwardObjectDestroyed(EventData, "S_EVENT_UNIT_LOST")
|
||||
end
|
||||
|
||||
--- Forward DCS S_EVENT_DEAD as object.destroyed.
|
||||
-- @param Core.Event#EVENTDATA EventData MOOSE-normalized DCS event data.
|
||||
function MOOSE_BRIDGE:OnEventDead(EventData)
|
||||
self:_ForwardObjectDestroyed(EventData, "S_EVENT_DEAD")
|
||||
end
|
||||
|
||||
--- Forward an attributed DCS kill without replacing UnitLost/Dead state events.
|
||||
-- @param Core.Event#EVENTDATA EventData MOOSE-normalized DCS event data.
|
||||
function MOOSE_BRIDGE:OnEventKill(EventData)
|
||||
local ok, err = pcall(function()
|
||||
if type(EventData) ~= "table" then error("Kill event data is missing") end
|
||||
local killer_name = EventData.IniUnitName or EventData.IniDCSUnitName
|
||||
local target_name = EventData.TgtUnitName or EventData.TgtDCSUnitName
|
||||
if not killer_name or not target_name then error("Kill event has no killer or target name") end
|
||||
|
||||
local target_is_static = Object and Object.Category
|
||||
and EventData.TgtObjectCategory == Object.Category.STATIC
|
||||
self:SendEvent("combat.kill", {
|
||||
dcs_event_id=EventData.id,
|
||||
dcs_event_name="S_EVENT_KILL",
|
||||
dcs_event_time=EventData.time,
|
||||
killer_object_id="UNIT:" .. tostring(killer_name),
|
||||
killer_group_id=EventData.IniGroupName and ("GROUP:" .. tostring(EventData.IniGroupName)) or nil,
|
||||
killer_coalition=self:_CoalitionToName(EventData.IniCoalition),
|
||||
killer_type=EventData.IniTypeName and tostring(EventData.IniTypeName) or nil,
|
||||
target_object_id=(target_is_static and "STATIC:" or "UNIT:") .. tostring(target_name),
|
||||
target_group_id=EventData.TgtGroupName and ("GROUP:" .. tostring(EventData.TgtGroupName)) or nil,
|
||||
target_coalition=self:_CoalitionToName(EventData.TgtCoalition),
|
||||
target_type=EventData.TgtTypeName and tostring(EventData.TgtTypeName) or nil,
|
||||
weapon_name=EventData.WeaponName and tostring(EventData.WeaponName) or nil,
|
||||
})
|
||||
end)
|
||||
if not ok then
|
||||
self:_Log("Failed to forward Kill event: " .. tostring(err))
|
||||
end
|
||||
end
|
||||
|
||||
--- Forward DCS S_EVENT_MISSION_END as the authoritative Python session boundary.
|
||||
-- Flush immediately because normal bridge scheduling stops with the mission.
|
||||
-- @param Core.Event#EVENTDATA EventData MOOSE-normalized DCS event data.
|
||||
function MOOSE_BRIDGE:OnEventMissionEnd(EventData)
|
||||
local ok, err = pcall(function()
|
||||
self:SendEvent("mission.ended", {
|
||||
dcs_event_id=EventData and EventData.id or nil,
|
||||
dcs_event_name="S_EVENT_MISSION_END",
|
||||
dcs_event_time=EventData and EventData.time or nil,
|
||||
reason="dcs_mission_end",
|
||||
})
|
||||
self:_FlushOutQueue()
|
||||
end)
|
||||
if not ok then
|
||||
self:_Log("Failed to forward MissionEnd event: " .. tostring(err))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,404 @@
|
||||
-- Optional INTEL snapshot and event extension for MOOSE Bridge.
|
||||
--
|
||||
-- MOOSE remains the owner of INTEL tactical logic. This extension only mirrors
|
||||
-- registered INTEL objects and forwards their FSM events to Python.
|
||||
|
||||
local function bridge_intel_safe_tostring(value)
|
||||
if value == nil then return nil end
|
||||
return tostring(value)
|
||||
end
|
||||
|
||||
local function bridge_intel_object_name(object)
|
||||
if not object then return nil end
|
||||
if MOOSE_BRIDGE and MOOSE_BRIDGE._ObjectName then
|
||||
local ok, value = pcall(function() return MOOSE_BRIDGE:_ObjectName(object) end)
|
||||
if ok and value then return value end
|
||||
end
|
||||
if object.alias then return tostring(object.alias) end
|
||||
if object.name then return tostring(object.name) end
|
||||
if object.Name then return tostring(object.Name) end
|
||||
if object.groupname then return tostring(object.groupname) end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function bridge_intel_auftrag_id(bridge, auftrag)
|
||||
if not auftrag then return nil end
|
||||
if bridge and bridge._AuftragObjectId then
|
||||
local ok, value = pcall(function() return bridge:_AuftragObjectId(auftrag) end)
|
||||
if ok and value then return value end
|
||||
end
|
||||
if auftrag.auftragsnummer then return "AUFTRAG:" .. tostring(auftrag.auftragsnummer) end
|
||||
if auftrag.name then return "AUFTRAG:" .. tostring(auftrag.name) end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function bridge_intel_point(bridge, value)
|
||||
if not value then return nil end
|
||||
if bridge and bridge._PointFromMooseObject then
|
||||
local ok, point = pcall(function() return bridge:_PointFromMooseObject(value) end)
|
||||
if ok and point then return point end
|
||||
end
|
||||
if type(value) == "table" and type(value.x) == "number" and type(value.z) == "number" then
|
||||
return value
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function bridge_intel_velocity(value)
|
||||
if type(value) ~= "table" then return nil end
|
||||
return {
|
||||
x=type(value.x) == "number" and value.x or nil,
|
||||
y=type(value.y) == "number" and value.y or nil,
|
||||
z=type(value.z) == "number" and value.z or nil,
|
||||
}
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_EnsureIntelRegistry()
|
||||
self.RegisteredIntels = self.RegisteredIntels or {}
|
||||
return self.RegisteredIntels
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:RegisterIntel(intel, name)
|
||||
if not intel then return self end
|
||||
local intel_name = name or bridge_intel_object_name(intel) or intel.alias
|
||||
if not intel_name then return self end
|
||||
intel_name = tostring(intel_name)
|
||||
self:_EnsureIntelRegistry()[intel_name] = intel
|
||||
self:_AttachIntelEventForwarders(intel, intel_name)
|
||||
if type(intel.SetAgentAuto) ~= "function" then error("INTEL:SetAgentAuto is not available") end
|
||||
intel:SetAgentAuto()
|
||||
return self
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:RegisterIntels(intels)
|
||||
if type(intels) ~= "table" then return self end
|
||||
for name, intel in pairs(intels) do self:RegisterIntel(intel, name) end
|
||||
return self
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_IntelObjectId(intel_name)
|
||||
return "INTEL:" .. tostring(intel_name)
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_IntelContactId(intel_name, contact)
|
||||
local name = contact and contact.groupname or nil
|
||||
if not name then return nil end
|
||||
return "INTELCONTACT:" .. tostring(intel_name) .. ":" .. tostring(name)
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_IntelClusterId(intel_name, cluster)
|
||||
local index = cluster and cluster.index or nil
|
||||
if not index then return nil end
|
||||
return "INTELCLUSTER:" .. tostring(intel_name) .. ":" .. tostring(index)
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_IntelContactTargetObjectId(contact)
|
||||
if not contact then return nil end
|
||||
local name = contact.groupname
|
||||
if not name then return nil end
|
||||
if contact.isStatic then return "STATIC:" .. tostring(name) end
|
||||
return "GROUP:" .. tostring(name)
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_IntelAgentCounts(intel)
|
||||
local detectionset = intel and intel.detectionset or nil
|
||||
if not detectionset then return 0, 0 end
|
||||
return detectionset:Count(), detectionset:CountAlive()
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_IntelAgentSnapshot(intel)
|
||||
local result = {}
|
||||
local detectionset = intel and intel.detectionset or nil
|
||||
if not detectionset then return result, 0, 0 end
|
||||
|
||||
for name, _ in pairs(detectionset.Set or {}) do
|
||||
result[#result + 1] = "GROUP:" .. tostring(name)
|
||||
end
|
||||
|
||||
local agent_count, alive_agent_count = self:_IntelAgentCounts(intel)
|
||||
return result, agent_count, alive_agent_count
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_ResolveRegisteredIntel(intel_id)
|
||||
local name = type(intel_id) == "string" and string.match(intel_id, "^INTEL:(.+)$") or nil
|
||||
if not name then return nil, nil, "Invalid INTEL object id: " .. tostring(intel_id) end
|
||||
local intel = self:_EnsureIntelRegistry()[name]
|
||||
if not intel then return nil, nil, "INTEL not registered: " .. name end
|
||||
return intel, name, nil
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_ResolveIntelAgent(agent_id)
|
||||
local prefix, name
|
||||
if type(agent_id) == "string" then prefix, name = string.match(agent_id, "^([^:]+):(.+)$") end
|
||||
if not prefix or not name then return nil, "Invalid agent object id: " .. tostring(agent_id) end
|
||||
|
||||
if prefix == "GROUP" then
|
||||
local group = GROUP and GROUP.FindByName and GROUP:FindByName(name) or nil
|
||||
if not group then return nil, "GROUP not found: " .. name end
|
||||
return group, nil
|
||||
end
|
||||
|
||||
if prefix == "OPSGROUP" then
|
||||
if not self._ResolveOpsGroupById then
|
||||
return nil, "OPSGROUP agents require MooseBridgeAuftragExecutionExtension.lua"
|
||||
end
|
||||
return self:_ResolveOpsGroupById(agent_id)
|
||||
end
|
||||
|
||||
return nil, "Agent must be GROUP:<name> or OPSGROUP:<name>"
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_BuildIntelContactSnapshotItem(intel_name, contact, source)
|
||||
if type(contact) ~= "table" then return nil end
|
||||
local point = bridge_intel_point(self, contact.position)
|
||||
local recce_name = bridge_intel_safe_tostring(contact.recce)
|
||||
local recce_unit = recce_name and UNIT and UNIT.FindByName and UNIT:FindByName(recce_name) or nil
|
||||
local recce_group = recce_unit and self:_SafeCall(recce_unit, "GetGroup") or nil
|
||||
local recce_group_name = recce_group and self:_SafeCall(recce_group, "GetName") or nil
|
||||
local item = {
|
||||
object_id=self:_IntelContactId(intel_name, contact),
|
||||
dcs_name=bridge_intel_safe_tostring(contact.groupname),
|
||||
object_type="INTELCONTACT",
|
||||
category=bridge_intel_safe_tostring(contact.ctype or contact.categoryname),
|
||||
source=source,
|
||||
intel_id=self:_IntelObjectId(intel_name),
|
||||
target_object_id=self:_IntelContactTargetObjectId(contact),
|
||||
typename=bridge_intel_safe_tostring(contact.typename),
|
||||
attribute=bridge_intel_safe_tostring(contact.attribute),
|
||||
category_id=contact.category,
|
||||
category_name=bridge_intel_safe_tostring(contact.categoryname),
|
||||
threat_level=contact.threatlevel,
|
||||
detected_time=contact.Tdetected,
|
||||
recce=recce_name,
|
||||
recce_unit_id=recce_name and "UNIT:" .. recce_name or nil,
|
||||
recce_group_id=recce_group_name and "GROUP:" .. tostring(recce_group_name) or nil,
|
||||
contact_type=bridge_intel_safe_tostring(contact.ctype),
|
||||
speed_mps=contact.speed,
|
||||
velocity=bridge_intel_velocity(contact.velocity),
|
||||
is_ground=contact.isground and true or false,
|
||||
is_ship=contact.isship and true or false,
|
||||
is_static=contact.isStatic and true or false,
|
||||
platform=bridge_intel_safe_tostring(contact.platform),
|
||||
heading=contact.heading,
|
||||
maneuvering=contact.maneuvering and true or false,
|
||||
altitude_m=contact.altitude,
|
||||
rcs=contact.rcs,
|
||||
mission_id=bridge_intel_auftrag_id(self, contact.mission),
|
||||
}
|
||||
if point then self:_AddPointFields(item, point) end
|
||||
return item
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_BuildIntelClusterSnapshotItem(intel_name, cluster, source)
|
||||
if type(cluster) ~= "table" then return nil end
|
||||
local point = bridge_intel_point(self, cluster.coordinate)
|
||||
local contact_ids = {}
|
||||
if type(cluster.Contacts) == "table" then
|
||||
for _, contact in pairs(cluster.Contacts) do
|
||||
local contact_id = self:_IntelContactId(intel_name, contact)
|
||||
if contact_id then contact_ids[#contact_ids + 1] = contact_id end
|
||||
end
|
||||
end
|
||||
local item = {
|
||||
object_id=self:_IntelClusterId(intel_name, cluster),
|
||||
dcs_name="Cluster " .. tostring(cluster.index or "?"),
|
||||
object_type="INTELCLUSTER",
|
||||
category=bridge_intel_safe_tostring(cluster.ctype),
|
||||
source=source,
|
||||
intel_id=self:_IntelObjectId(intel_name),
|
||||
index=cluster.index,
|
||||
size=cluster.size,
|
||||
contact_ids=contact_ids,
|
||||
threat_level_max=cluster.threatlevelMax,
|
||||
threat_level_sum=cluster.threatlevelSum,
|
||||
threat_level_avg=cluster.threatlevelAve,
|
||||
contact_type=bridge_intel_safe_tostring(cluster.ctype),
|
||||
altitude_m=cluster.altitude,
|
||||
mission_id=bridge_intel_auftrag_id(self, cluster.mission),
|
||||
}
|
||||
if point then self:_AddPointFields(item, point) end
|
||||
return item
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_BuildIntelSnapshotItem(intel_name, intel, source)
|
||||
local contacts = self:_SafeCall(intel, "GetContactTable") or intel.Contacts or {}
|
||||
local clusters = self:_SafeCall(intel, "GetClusterTable") or intel.Clusters or {}
|
||||
local agent_ids, agent_count, alive_agent_count = self:_IntelAgentSnapshot(intel)
|
||||
return {
|
||||
object_id=self:_IntelObjectId(intel_name),
|
||||
dcs_name=tostring(intel_name),
|
||||
object_type="INTEL",
|
||||
category="INTEL",
|
||||
source=source,
|
||||
alias=bridge_intel_safe_tostring(intel.alias),
|
||||
coalition=self:_CoalitionToName(intel.coalition),
|
||||
state=self:_SafeCallArg(intel, "GetState") or nil,
|
||||
is_running=self:_SafeCallArg(intel, "Is", "Running") and true or false,
|
||||
cluster_analysis=intel.clusteranalysis and true or false,
|
||||
cluster_markers=intel.clustermarkers and true or false,
|
||||
cluster_arrows=intel.clusterarrows and true or false,
|
||||
cluster_radius_m=intel.clusterradius,
|
||||
detect_statics=intel.detectStatics and true or false,
|
||||
detect_accoustic=intel.DetectAccoustic and true or false,
|
||||
detect_accoustic_radius_m=intel.DetectAccousticRadius,
|
||||
doppler_radar=intel.DopplerRadar and true or false,
|
||||
contact_count=type(contacts) == "table" and #contacts or 0,
|
||||
cluster_count=type(clusters) == "table" and #clusters or 0,
|
||||
agent_count=agent_count,
|
||||
alive_agent_count=alive_agent_count,
|
||||
agent_ids=agent_ids,
|
||||
}
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:BuildIntelSnapshot()
|
||||
local result = {}
|
||||
for name, intel in pairs(self:_EnsureIntelRegistry()) do
|
||||
local ok, item = pcall(function() return self:_BuildIntelSnapshotItem(name, intel, "registered") end)
|
||||
if ok and item then result[#result + 1] = item end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:BuildIntelContactSnapshot()
|
||||
local result = {}
|
||||
for name, intel in pairs(self:_EnsureIntelRegistry()) do
|
||||
local contacts = self:_SafeCall(intel, "GetContactTable") or intel.Contacts or {}
|
||||
if type(contacts) == "table" then
|
||||
for _, contact in pairs(contacts) do
|
||||
local ok, item = pcall(function() return self:_BuildIntelContactSnapshotItem(name, contact, "registered") end)
|
||||
if ok and item and item.object_id then result[#result + 1] = item end
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:BuildIntelClusterSnapshot()
|
||||
local result = {}
|
||||
for name, intel in pairs(self:_EnsureIntelRegistry()) do
|
||||
local clusters = self:_SafeCall(intel, "GetClusterTable") or intel.Clusters or {}
|
||||
if type(clusters) == "table" then
|
||||
for _, cluster in pairs(clusters) do
|
||||
local ok, item = pcall(function() return self:_BuildIntelClusterSnapshotItem(name, cluster, "registered") end)
|
||||
if ok and item and item.object_id then result[#result + 1] = item end
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_SendIntelEvent(event_name, intel_name, fsm_event, from_state, to_state, item_kind, item)
|
||||
local payload = {
|
||||
event=event_name,
|
||||
intel_id=self:_IntelObjectId(intel_name),
|
||||
fsm_event=fsm_event,
|
||||
from_state=from_state,
|
||||
to_state=to_state,
|
||||
}
|
||||
if item_kind == "contact" then
|
||||
payload.contact = item
|
||||
payload.contact_id = item and item.object_id or nil
|
||||
payload.target_object_id = item and item.target_object_id or nil
|
||||
elseif item_kind == "cluster" then
|
||||
payload.cluster = item
|
||||
payload.cluster_id = item and item.object_id or nil
|
||||
end
|
||||
self:SendEvent(event_name, payload)
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_AttachIntelEventForwarders(intel, intel_name)
|
||||
if type(intel) ~= "table" or intel.MooseBridgeIntelEventsRegistered then return self end
|
||||
intel.MooseBridgeIntelEventsRegistered = true
|
||||
local bridge = self
|
||||
local previous_new_contact = intel.OnAfterNewContact
|
||||
local previous_lost_contact = intel.OnAfterLostContact
|
||||
local previous_new_cluster = intel.OnAfterNewCluster
|
||||
local previous_lost_cluster = intel.OnAfterLostCluster
|
||||
|
||||
intel.OnAfterNewContact = function(intel_self, From, Event, To, Contact)
|
||||
if type(previous_new_contact) == "function" then pcall(previous_new_contact, intel_self, From, Event, To, Contact) end
|
||||
local item = bridge:_BuildIntelContactSnapshotItem(intel_name, Contact, "event")
|
||||
bridge:_SendIntelEvent("intel.new_contact", intel_name, Event, From, To, "contact", item)
|
||||
end
|
||||
|
||||
intel.OnAfterLostContact = function(intel_self, From, Event, To, Contact)
|
||||
if type(previous_lost_contact) == "function" then pcall(previous_lost_contact, intel_self, From, Event, To, Contact) end
|
||||
local item = bridge:_BuildIntelContactSnapshotItem(intel_name, Contact, "event")
|
||||
bridge:_SendIntelEvent("intel.lost_contact", intel_name, Event, From, To, "contact", item)
|
||||
end
|
||||
|
||||
intel.OnAfterNewCluster = function(intel_self, From, Event, To, Cluster)
|
||||
if type(previous_new_cluster) == "function" then pcall(previous_new_cluster, intel_self, From, Event, To, Cluster) end
|
||||
local item = bridge:_BuildIntelClusterSnapshotItem(intel_name, Cluster, "event")
|
||||
bridge:_SendIntelEvent("intel.new_cluster", intel_name, Event, From, To, "cluster", item)
|
||||
end
|
||||
|
||||
intel.OnAfterLostCluster = function(intel_self, From, Event, To, Cluster, Mission)
|
||||
if type(previous_lost_cluster) == "function" then pcall(previous_lost_cluster, intel_self, From, Event, To, Cluster, Mission) end
|
||||
local item = bridge:_BuildIntelClusterSnapshotItem(intel_name, Cluster, "event")
|
||||
if item and not item.mission_id then item.mission_id = bridge_intel_auftrag_id(bridge, Mission) end
|
||||
bridge:_SendIntelEvent("intel.lost_cluster", intel_name, Event, From, To, "cluster", item)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
local _moose_bridge_base_register_default_commands_for_intel = MOOSE_BRIDGE.RegisterDefaultCommands
|
||||
|
||||
function MOOSE_BRIDGE:RegisterDefaultCommands()
|
||||
_moose_bridge_base_register_default_commands_for_intel(self)
|
||||
|
||||
local previous_snapshot_all = self.CommandHandlers["snapshot.all"]
|
||||
|
||||
self:RegisterCommand("snapshot.intels", function(cmd)
|
||||
local intels = self:BuildIntelSnapshot()
|
||||
self:SendSnapshot("intels", {intels=intels})
|
||||
return {kind="intels", count=#intels}
|
||||
end)
|
||||
|
||||
self:RegisterCommand("intel.add_agent", function(cmd)
|
||||
local params = self:_CommandParams(cmd)
|
||||
local intel, intel_name, intel_err = self:_ResolveRegisteredIntel(params.intel_id)
|
||||
if not intel then error(intel_err) end
|
||||
local agent, agent_err = self:_ResolveIntelAgent(params.agent_id)
|
||||
if not agent then error(agent_err) end
|
||||
|
||||
intel:AddAgent(agent)
|
||||
local agent_count, alive_agent_count = self:_IntelAgentCounts(intel)
|
||||
return {
|
||||
action="intel.add_agent",
|
||||
intel_id=self:_IntelObjectId(intel_name),
|
||||
agent_id=params.agent_id,
|
||||
agent_count=agent_count,
|
||||
alive_agent_count=alive_agent_count,
|
||||
}
|
||||
end)
|
||||
|
||||
self:RegisterCommand("snapshot.intel_contacts", function(cmd)
|
||||
local contacts = self:BuildIntelContactSnapshot()
|
||||
self:SendSnapshot("intel_contacts", {intel_contacts=contacts})
|
||||
return {kind="intel_contacts", count=#contacts}
|
||||
end)
|
||||
|
||||
self:RegisterCommand("snapshot.intel_clusters", function(cmd)
|
||||
local clusters = self:BuildIntelClusterSnapshot()
|
||||
self:SendSnapshot("intel_clusters", {intel_clusters=clusters})
|
||||
return {kind="intel_clusters", count=#clusters}
|
||||
end)
|
||||
|
||||
if previous_snapshot_all then
|
||||
self:RegisterCommand("snapshot.all", function(cmd)
|
||||
local result = previous_snapshot_all(cmd) or {}
|
||||
local intels = self:BuildIntelSnapshot()
|
||||
local contacts = self:BuildIntelContactSnapshot()
|
||||
local clusters = self:BuildIntelClusterSnapshot()
|
||||
self:SendSnapshot("intels", {intels=intels})
|
||||
self:SendSnapshot("intel_contacts", {intel_contacts=contacts})
|
||||
self:SendSnapshot("intel_clusters", {intel_clusters=clusters})
|
||||
result.intels = #intels
|
||||
result.intel_contacts = #contacts
|
||||
result.intel_clusters = #clusters
|
||||
return result
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,258 @@
|
||||
--- Minimal JSON helper for the MOOSE Bridge V1 prototype.
|
||||
-- This is deliberately small and covers the V1 command/ack/heartbeat/snapshot payloads.
|
||||
|
||||
MOOSE_BRIDGE_JSON = MOOSE_BRIDGE_JSON or {}
|
||||
local json = MOOSE_BRIDGE_JSON
|
||||
|
||||
local function escape(value)
|
||||
value = tostring(value or "")
|
||||
value = value:gsub('\\', '\\\\')
|
||||
value = value:gsub('"', '\\"')
|
||||
value = value:gsub('\n', '\\n')
|
||||
value = value:gsub('\r', '\\r')
|
||||
value = value:gsub('\t', '\\t')
|
||||
return value
|
||||
end
|
||||
|
||||
local function is_array(value)
|
||||
if type(value) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
local max_index = 0
|
||||
local count = 0
|
||||
|
||||
for key, _ in pairs(value) do
|
||||
if type(key) ~= "number" or key < 1 or key % 1 ~= 0 then
|
||||
return false
|
||||
end
|
||||
if key > max_index then
|
||||
max_index = key
|
||||
end
|
||||
count = count + 1
|
||||
end
|
||||
|
||||
return count == max_index
|
||||
end
|
||||
|
||||
local function encode_value(value)
|
||||
local t = type(value)
|
||||
|
||||
if value == nil then
|
||||
return "null"
|
||||
elseif t == "boolean" then
|
||||
return value and "true" or "false"
|
||||
elseif t == "number" then
|
||||
return tostring(value)
|
||||
elseif t == "string" then
|
||||
return '"' .. escape(value) .. '"'
|
||||
elseif t == "table" then
|
||||
local parts = {}
|
||||
|
||||
if is_array(value) then
|
||||
for index = 1, #value do
|
||||
parts[#parts + 1] = encode_value(value[index])
|
||||
end
|
||||
return "[" .. table.concat(parts, ",") .. "]"
|
||||
end
|
||||
|
||||
for k, v in pairs(value) do
|
||||
parts[#parts + 1] = '"' .. escape(k) .. '":' .. encode_value(v)
|
||||
end
|
||||
return "{" .. table.concat(parts, ",") .. "}"
|
||||
end
|
||||
|
||||
return '"' .. escape(value) .. '"'
|
||||
end
|
||||
|
||||
function json.encode(value)
|
||||
return encode_value(value)
|
||||
end
|
||||
|
||||
local function decode_error(text, index, message)
|
||||
error("JSON decode error at byte " .. tostring(index) .. ": " .. message .. " near " .. string.format("%q", text:sub(index, index + 20)))
|
||||
end
|
||||
|
||||
local function skip_ws(text, index)
|
||||
while index <= #text do
|
||||
local char = text:sub(index, index)
|
||||
if char ~= " " and char ~= "\n" and char ~= "\r" and char ~= "\t" then break end
|
||||
index = index + 1
|
||||
end
|
||||
return index
|
||||
end
|
||||
|
||||
local function utf8_char(codepoint)
|
||||
if codepoint <= 0x7F then
|
||||
return string.char(codepoint)
|
||||
elseif codepoint <= 0x7FF then
|
||||
return string.char(
|
||||
0xC0 + math.floor(codepoint / 0x40),
|
||||
0x80 + (codepoint % 0x40)
|
||||
)
|
||||
elseif codepoint <= 0xFFFF then
|
||||
return string.char(
|
||||
0xE0 + math.floor(codepoint / 0x1000),
|
||||
0x80 + (math.floor(codepoint / 0x40) % 0x40),
|
||||
0x80 + (codepoint % 0x40)
|
||||
)
|
||||
elseif codepoint <= 0x10FFFF then
|
||||
return string.char(
|
||||
0xF0 + math.floor(codepoint / 0x40000),
|
||||
0x80 + (math.floor(codepoint / 0x1000) % 0x40),
|
||||
0x80 + (math.floor(codepoint / 0x40) % 0x40),
|
||||
0x80 + (codepoint % 0x40)
|
||||
)
|
||||
end
|
||||
return "?"
|
||||
end
|
||||
|
||||
local parse_value
|
||||
|
||||
local function parse_string(text, index)
|
||||
if text:sub(index, index) ~= '"' then decode_error(text, index, "expected string") end
|
||||
index = index + 1
|
||||
local parts = {}
|
||||
local start = index
|
||||
|
||||
while index <= #text do
|
||||
local char = text:sub(index, index)
|
||||
if char == '"' then
|
||||
parts[#parts + 1] = text:sub(start, index - 1)
|
||||
return table.concat(parts), index + 1
|
||||
elseif char == "\\" then
|
||||
parts[#parts + 1] = text:sub(start, index - 1)
|
||||
local escape_char = text:sub(index + 1, index + 1)
|
||||
if escape_char == '"' or escape_char == "\\" or escape_char == "/" then
|
||||
parts[#parts + 1] = escape_char
|
||||
index = index + 2
|
||||
elseif escape_char == "b" then
|
||||
parts[#parts + 1] = "\b"
|
||||
index = index + 2
|
||||
elseif escape_char == "f" then
|
||||
parts[#parts + 1] = "\f"
|
||||
index = index + 2
|
||||
elseif escape_char == "n" then
|
||||
parts[#parts + 1] = "\n"
|
||||
index = index + 2
|
||||
elseif escape_char == "r" then
|
||||
parts[#parts + 1] = "\r"
|
||||
index = index + 2
|
||||
elseif escape_char == "t" then
|
||||
parts[#parts + 1] = "\t"
|
||||
index = index + 2
|
||||
elseif escape_char == "u" then
|
||||
local hex = text:sub(index + 2, index + 5)
|
||||
local codepoint = tonumber(hex, 16)
|
||||
if not codepoint then decode_error(text, index, "invalid unicode escape") end
|
||||
index = index + 6
|
||||
|
||||
if codepoint >= 0xD800 and codepoint <= 0xDBFF and text:sub(index, index + 1) == "\\u" then
|
||||
local low = tonumber(text:sub(index + 2, index + 5), 16)
|
||||
if low and low >= 0xDC00 and low <= 0xDFFF then
|
||||
codepoint = 0x10000 + ((codepoint - 0xD800) * 0x400) + (low - 0xDC00)
|
||||
index = index + 6
|
||||
end
|
||||
end
|
||||
|
||||
parts[#parts + 1] = utf8_char(codepoint)
|
||||
else
|
||||
decode_error(text, index, "invalid escape sequence")
|
||||
end
|
||||
start = index
|
||||
else
|
||||
index = index + 1
|
||||
end
|
||||
end
|
||||
|
||||
decode_error(text, index, "unterminated string")
|
||||
end
|
||||
|
||||
local function parse_number(text, index)
|
||||
local start = index
|
||||
if text:sub(index, index) == "-" then index = index + 1 end
|
||||
while text:sub(index, index):match("%d") do index = index + 1 end
|
||||
if text:sub(index, index) == "." then
|
||||
index = index + 1
|
||||
while text:sub(index, index):match("%d") do index = index + 1 end
|
||||
end
|
||||
local exponent = text:sub(index, index)
|
||||
if exponent == "e" or exponent == "E" then
|
||||
index = index + 1
|
||||
local sign = text:sub(index, index)
|
||||
if sign == "+" or sign == "-" then index = index + 1 end
|
||||
while text:sub(index, index):match("%d") do index = index + 1 end
|
||||
end
|
||||
local raw = text:sub(start, index - 1)
|
||||
local value = tonumber(raw)
|
||||
if value == nil then decode_error(text, start, "invalid number") end
|
||||
return value, index
|
||||
end
|
||||
|
||||
local function parse_array(text, index)
|
||||
index = skip_ws(text, index + 1)
|
||||
local result = {}
|
||||
if text:sub(index, index) == "]" then return result, index + 1 end
|
||||
|
||||
while index <= #text do
|
||||
local value
|
||||
value, index = parse_value(text, index)
|
||||
result[#result + 1] = value
|
||||
index = skip_ws(text, index)
|
||||
local char = text:sub(index, index)
|
||||
if char == "]" then return result, index + 1 end
|
||||
if char ~= "," then decode_error(text, index, "expected ',' or ']'") end
|
||||
index = skip_ws(text, index + 1)
|
||||
end
|
||||
|
||||
decode_error(text, index, "unterminated array")
|
||||
end
|
||||
|
||||
local function parse_object(text, index)
|
||||
index = skip_ws(text, index + 1)
|
||||
local result = {}
|
||||
if text:sub(index, index) == "}" then return result, index + 1 end
|
||||
|
||||
while index <= #text do
|
||||
local key
|
||||
key, index = parse_string(text, index)
|
||||
index = skip_ws(text, index)
|
||||
if text:sub(index, index) ~= ":" then decode_error(text, index, "expected ':'") end
|
||||
index = skip_ws(text, index + 1)
|
||||
local value
|
||||
value, index = parse_value(text, index)
|
||||
result[key] = value
|
||||
index = skip_ws(text, index)
|
||||
local char = text:sub(index, index)
|
||||
if char == "}" then return result, index + 1 end
|
||||
if char ~= "," then decode_error(text, index, "expected ',' or '}'") end
|
||||
index = skip_ws(text, index + 1)
|
||||
end
|
||||
|
||||
decode_error(text, index, "unterminated object")
|
||||
end
|
||||
|
||||
parse_value = function(text, index)
|
||||
index = skip_ws(text, index)
|
||||
local char = text:sub(index, index)
|
||||
if char == '"' then return parse_string(text, index) end
|
||||
if char == "{" then return parse_object(text, index) end
|
||||
if char == "[" then return parse_array(text, index) end
|
||||
if char == "-" or char:match("%d") then return parse_number(text, index) end
|
||||
if text:sub(index, index + 3) == "true" then return true, index + 4 end
|
||||
if text:sub(index, index + 4) == "false" then return false, index + 5 end
|
||||
if text:sub(index, index + 3) == "null" then return nil, index + 4 end
|
||||
decode_error(text, index, "unexpected value")
|
||||
end
|
||||
|
||||
function json.decode(text)
|
||||
if type(text) ~= "string" then error("JSON decode expects a string") end
|
||||
local value, index = parse_value(text, 1)
|
||||
index = skip_ws(text, index)
|
||||
if index <= #text then decode_error(text, index, "trailing characters") end
|
||||
if type(value) ~= "table" then error("JSON command must decode to an object") end
|
||||
if type(value.params) ~= "table" then value.params = {} end
|
||||
return value
|
||||
end
|
||||
|
||||
return json
|
||||
@@ -0,0 +1,105 @@
|
||||
-- Optional AIRWING payload snapshot extension for MOOSE Bridge.
|
||||
--
|
||||
-- Load after MooseBridge.lua when AIRWING payload availability should be included
|
||||
-- in COHORT snapshots. The core Python advisory layer can then reject AIRWING
|
||||
-- candidates without a compatible payload for the requested AUFTRAG type.
|
||||
|
||||
if not MOOSE_BRIDGE then error("Load MooseBridge.lua before MooseBridgePayloadExtension.lua") end
|
||||
|
||||
local function bridge_safe_tostring(value)
|
||||
if value == nil then return "nil" end
|
||||
return tostring(value)
|
||||
end
|
||||
|
||||
local function bridge_string_or_nil(value)
|
||||
if value == nil then return nil end
|
||||
return tostring(value)
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_CohortUnitType(cohort)
|
||||
if not cohort then return nil end
|
||||
local unit_type = self:_SafeCall(cohort, "GetUnitType")
|
||||
if not unit_type then unit_type = self:_SafeCall(cohort, "GetTypeName") end
|
||||
if not unit_type then unit_type = self:_SafeCall(cohort, "GetType") end
|
||||
if not unit_type then unit_type = cohort.unittype or cohort.unitType or cohort.aircrafttype or cohort.AircraftType or cohort.type end
|
||||
return unit_type and bridge_safe_tostring(unit_type) or nil
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_PayloadPerformance(payload, mission_type)
|
||||
if type(payload) ~= "table" or type(payload.capabilities) ~= "table" then return nil end
|
||||
for _, capability in pairs(payload.capabilities) do
|
||||
if type(capability) == "table" and capability.MissionType == mission_type then
|
||||
return self:_NumberOrNil(capability.Performance)
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_SummarizePayload(payload, mission_type)
|
||||
if type(payload) ~= "table" then return nil end
|
||||
local performance = self:_PayloadPerformance(payload, mission_type)
|
||||
return {
|
||||
uid=payload.uid,
|
||||
unitname=bridge_string_or_nil(payload.unitname),
|
||||
aircrafttype=bridge_string_or_nil(payload.aircrafttype),
|
||||
navail=self:_NumberOrNil(payload.navail),
|
||||
unlimited=self:_BoolOrFalse(payload.unlimited),
|
||||
performance=performance,
|
||||
}
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_PayloadAvailabilityForMission(airwing, unit_type, mission_type)
|
||||
local payloads = self:_SafeCallArg(airwing, "_FilterPlayloads", unit_type, mission_type)
|
||||
if payloads == nil then payloads = self:_SafeCallArg(airwing, "_FilterPayloads", unit_type, mission_type) end
|
||||
|
||||
local result = {
|
||||
available_count=0,
|
||||
total_available=0,
|
||||
unlimited_count=0,
|
||||
best_performance=nil,
|
||||
payloads={},
|
||||
}
|
||||
|
||||
if type(payloads) ~= "table" then return result end
|
||||
|
||||
for _, payload in pairs(payloads) do
|
||||
local item = self:_SummarizePayload(payload, mission_type)
|
||||
if item then
|
||||
result.payloads[#result.payloads + 1] = item
|
||||
result.available_count = result.available_count + 1
|
||||
if item.unlimited then result.unlimited_count = result.unlimited_count + 1 end
|
||||
if item.navail then result.total_available = result.total_available + item.navail end
|
||||
if item.performance and (not result.best_performance or item.performance > result.best_performance) then
|
||||
result.best_performance = item.performance
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_CollectPayloadAvailability(cohort, mission_types)
|
||||
local result = {}
|
||||
if not cohort or type(mission_types) ~= "table" then return result end
|
||||
local airwing = cohort.legion
|
||||
if not airwing or not self:_SafeCall(airwing, "IsAirwing") then return result end
|
||||
local unit_type = self:_CohortUnitType(cohort)
|
||||
if not unit_type then return result end
|
||||
|
||||
for _, mission_type in pairs(mission_types) do
|
||||
result[bridge_safe_tostring(mission_type)] = self:_PayloadAvailabilityForMission(airwing, unit_type, mission_type)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local _moose_bridge_base_build_cohort_snapshot_item = MOOSE_BRIDGE._BuildCohortSnapshotItem
|
||||
|
||||
function MOOSE_BRIDGE:_BuildCohortSnapshotItem(cohort_name, cohort, source)
|
||||
local item = _moose_bridge_base_build_cohort_snapshot_item(self, cohort_name, cohort, source)
|
||||
if not item then return nil end
|
||||
local unit_type = self:_CohortUnitType(cohort)
|
||||
item.unit_type = unit_type
|
||||
item.payloads_by_mission = self:_CollectPayloadAvailability(cohort, item.mission_types)
|
||||
return item
|
||||
end
|
||||
@@ -0,0 +1,73 @@
|
||||
-- Optional connection tuning extension for MOOSE Bridge.
|
||||
--
|
||||
-- Load after MooseBridge.lua and before creating the bridge instance. It reduces
|
||||
-- DCS main-thread stalls when the Python server is not running by shortening the
|
||||
-- blocking LuaSocket connect timeout and using less aggressive retry defaults.
|
||||
|
||||
if not MOOSE_BRIDGE then error("Load MooseBridge.lua before MooseBridgeSocketTuningExtension.lua") end
|
||||
|
||||
local function bridge_tuning_mission_time()
|
||||
if timer and timer.getTime then return timer.getTime() end
|
||||
return 0
|
||||
end
|
||||
|
||||
local function bridge_tuning_safe_tostring(value)
|
||||
if value == nil then return "nil" end
|
||||
return tostring(value)
|
||||
end
|
||||
|
||||
local function bridge_should_log_connect_failure(bridge, err, now)
|
||||
if bridge.LogConnectFailures == false then return false end
|
||||
if err == "timeout" and bridge.LogConnectTimeouts ~= true then return false end
|
||||
|
||||
local interval = bridge.ConnectFailureLogInterval or 60
|
||||
if bridge.LastConnectFailureLog and now - bridge.LastConnectFailureLog < interval then return false end
|
||||
|
||||
bridge.LastConnectFailureLog = now
|
||||
return true
|
||||
end
|
||||
|
||||
local _moose_bridge_tuning_base_new = MOOSE_BRIDGE.New
|
||||
|
||||
function MOOSE_BRIDGE:New(host, port)
|
||||
local bridge = _moose_bridge_tuning_base_new(self, host, port)
|
||||
|
||||
-- Keep idle bridge retries low-impact when the Python server is not listening.
|
||||
bridge.ConnectTimeout = bridge.ConnectTimeout or 0.02
|
||||
if bridge.ConnectRetryDelay == nil or bridge.ConnectRetryDelay == 5 then bridge.ConnectRetryDelay = 10 end
|
||||
if bridge.TickInterval == nil or bridge.TickInterval == 0.2 then bridge.TickInterval = 0.5 end
|
||||
if bridge.HeartbeatInterval == nil or bridge.HeartbeatInterval == 5 then bridge.HeartbeatInterval = 10 end
|
||||
|
||||
-- Keep expected idle reconnect timeouts out of dcs.log by default.
|
||||
if bridge.LogConnectFailures == nil then bridge.LogConnectFailures = true end
|
||||
if bridge.LogConnectTimeouts == nil then bridge.LogConnectTimeouts = false end
|
||||
bridge.ConnectFailureLogInterval = bridge.ConnectFailureLogInterval or 60
|
||||
bridge.LastConnectFailureLog = nil
|
||||
|
||||
return bridge
|
||||
end
|
||||
|
||||
function MOOSE_BRIDGE:_Connect()
|
||||
local now = bridge_tuning_mission_time()
|
||||
if now - self.LastConnectAttempt < (self.ConnectRetryDelay or 10) then return end
|
||||
self.LastConnectAttempt = now
|
||||
|
||||
local lib = require("socket")
|
||||
local conn = lib.tcp()
|
||||
conn:settimeout(self.ConnectTimeout or 0.02)
|
||||
|
||||
local ok, err = conn:connect(self.Host, self.Port)
|
||||
if not ok then
|
||||
if bridge_should_log_connect_failure(self, err, now) then
|
||||
self:_Log("Connect failed: " .. bridge_tuning_safe_tostring(err))
|
||||
end
|
||||
conn:close()
|
||||
return
|
||||
end
|
||||
|
||||
-- All regular bridge IO is polled from the scheduler tick and must not block DCS.
|
||||
conn:settimeout(0)
|
||||
self.Socket = conn
|
||||
self.Connected = true
|
||||
self:_Log("Connected to Python bridge")
|
||||
end
|
||||
@@ -131,14 +131,24 @@ ENUMS.WeaponFlag={
|
||||
Cannons = 805306368, -- GUN_POD + BuiltInCannon
|
||||
--- Torpedo
|
||||
Torpedo = 4294967296,
|
||||
--- Decoy
|
||||
Decoys = 8589934592,
|
||||
--- Shell
|
||||
SmokeShell = 17179869184,
|
||||
IlluminationShell = 34359738368,
|
||||
MarkerShell = 51539607552,
|
||||
MarkerWeapon = 51539620864,
|
||||
SubmunitionDispenserShell = 68719476736,
|
||||
ConventionalShell = 206963736576,
|
||||
---
|
||||
-- Even More Genral
|
||||
Auto = 3221225470, -- Any Weapon (AnyBomb + AnyRocket + AnyMissile + Cannons)
|
||||
AutoDCS = 1073741822, -- Something if often see
|
||||
AnyAG = 2956984318, -- Any Air-To-Ground Weapon
|
||||
AnyAA = 264241152, -- Any Air-To-Air Weapon
|
||||
AnyUnguided = 2952822768, -- Any Unguided Weapon
|
||||
AnyGuided = 268402702, -- Any Guided Weapon
|
||||
Auto = 265214230526, -- Any Weapon (AnyBomb + AnyRocket + AnyMissile + Cannons + Torpedos)
|
||||
AutoDCS = 1073741822, -- Something if often see
|
||||
AnyAG = 2956984318, -- Any Air-To-Ground Weapon
|
||||
AnyAA = 264241152, -- Any Air-To-Air Weapon
|
||||
AnyUnguided = 2952822768, -- Any Unguided Weapon
|
||||
AnyGuided = 268402702, -- Any Guided Weapon
|
||||
AnyShell = 258503344128, -- Any Shell
|
||||
}
|
||||
|
||||
--- Weapon types by category. See the [Weapon Flag](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag) enumerator on hoggit wiki.
|
||||
@@ -220,11 +230,11 @@ ENUMS.WeaponType.Torpedo={
|
||||
}
|
||||
ENUMS.WeaponType.Any={
|
||||
-- General combinations
|
||||
Weapon = 3221225470, -- Any Weapon (AnyBomb + AnyRocket + AnyMissile + Cannons)
|
||||
AG = 2956984318, -- Any Air-To-Ground Weapon
|
||||
AA = 264241152, -- Any Air-To-Air Weapon
|
||||
Unguided = 2952822768, -- Any Unguided Weapon
|
||||
Guided = 268402702, -- Any Guided Weapon
|
||||
Weapon = 265214230526, -- Any Weapon (AnyBomb + AnyRocket + AnyMissile + Cannons + Torpedos)
|
||||
AG = 2956984318, -- Any Air-To-Ground Weapon
|
||||
AA = 264241152, -- Any Air-To-Air Weapon
|
||||
Unguided = 2952822768, -- Any Unguided Weapon
|
||||
Guided = 268402702, -- Any Guided Weapon
|
||||
}
|
||||
|
||||
|
||||
@@ -513,7 +523,7 @@ ENUMS.ReportingName =
|
||||
Tornado = "Tornado",
|
||||
-- Transport / Bomber / Others
|
||||
Atlas = "A400",
|
||||
Lancer = "B1-B",
|
||||
Lancer = "B-1B",
|
||||
Stratofortress = "B-52H",
|
||||
Herc = "C-130",
|
||||
Hercules = "C-130J-30",
|
||||
@@ -558,7 +568,7 @@ ENUMS.ReportingName =
|
||||
Apache = "AH-64",
|
||||
Chinook = "CH-47",
|
||||
Sea_Stallion = "CH-53",
|
||||
Kiowa = "OH-58",
|
||||
Kiowa = "OH58D",
|
||||
Seahawk = "SH-60",
|
||||
Blackhawk = "UH-60",
|
||||
Sea_King = "S-61",
|
||||
@@ -1798,13 +1808,21 @@ ENUMS.Storage.weapons.missiles.Igla_S = 'weapons.missiles.Igla_S'
|
||||
ENUMS.Storage.weapons.gunmounts.AKAN_NO_TRC = 'weapons.gunmounts.{AKAN_NO_TRC}'
|
||||
ENUMS.Storage.weapons.gunmounts.AKAN = 'weapons.gunmounts.{AKAN}'
|
||||
ENUMS.Storage.weapons.shells.M882_9x19 = 'weapons.shells.9x19_m882'
|
||||
ENUMS.Storage.weapons.droptanks.fuel_tank_370gal = "weapons.droptanks.fuel_tank_370gal"
|
||||
ENUMS.Storage.weapons.droptanks.fuel_tank_300gal = "weapons.droptanks.fuel_tank_300gal"
|
||||
|
||||
-- NEW
|
||||
ENUMS.Storage.weapons.adapters.HB_F_4E_ORD_LAU_77 = 'weapons.adapters.HB_F-4E_ORD_LAU_77'
|
||||
ENUMS.Storage.weapons.adapters.hb_a_6e_lau7_adu299 = 'weapons.adapters.hb_a-6e_lau7_adu299'
|
||||
ENUMS.Storage.weapons.adapters.CHAP_AIM92LN = 'weapons.adapters.CHAP_AIM92LN'
|
||||
ENUMS.Storage.weapons.adapters.CHAP_HOT3LN = 'weapons.adapters.CHAP_HOT3LN'
|
||||
ENUMS.Storage.weapons.adapters.CHAP_FZ225 = 'weapons.adapters.CHAP_FZ225'
|
||||
|
||||
ENUMS.Storage.weapons.bombs.AH6_SMOKE_BLUE = 'weapons.bombs.AH6_SMOKE_BLUE'
|
||||
ENUMS.Storage.weapons.bombs.AH6_SMOKE_GREEN = 'weapons.bombs.AH6_SMOKE_GREEN'
|
||||
ENUMS.Storage.weapons.bombs.AH6_SMOKE_RED = 'weapons.bombs.AH6_SMOKE_RED'
|
||||
ENUMS.Storage.weapons.bombs.AH6_SMOKE_YELLOW = 'weapons.bombs.AH6_SMOKE_YELLOW'
|
||||
ENUMS.Storage.weapons.bombs.AO_25SL = 'weapons.bombs.AO_25SL'
|
||||
|
||||
ENUMS.Storage.weapons.missiles.HB_AGM_78 = 'weapons.missiles.HB_AGM_78'
|
||||
ENUMS.Storage.weapons.missiles.V_1 = 'weapons.missiles.V-1'
|
||||
@@ -1851,6 +1869,10 @@ ENUMS.Storage.weapons.shells.M42A1_HE = 'weapons.shells.M42A1_HE'
|
||||
ENUMS.Storage.weapons.shells.HE_20x138B = 'weapons.shells.20x138B_HE'
|
||||
ENUMS.Storage.weapons.shells.HE_37x263 = 'weapons.shells.37x263_HE'
|
||||
ENUMS.Storage.weapons.shells.HE_2A20_115mm = 'weapons.shells.2A20_115mm_HE'
|
||||
ENUMS.Storage.weapons.shells.AP_20x99R = 'weapons.shells.20x99R_AP'
|
||||
ENUMS.Storage.weapons.shells.IranFAC_DShK_API_T = 'weapons.shells.IranFAC_DShK_API_T'
|
||||
ENUMS.Storage.weapons.shells.IranFAC_DShK_API = 'weapons.shells.IranFAC_DShK_API'
|
||||
ENUMS.Storage.weapons.shells.HE_T_20x99R = 'weapons.shells.20x99R_HE_T'
|
||||
|
||||
ENUMS.Storage.weapons.gunmounts.B17_TailTurret_M2_L = 'weapons.gunmounts.B17_TailTurret_M2_L'
|
||||
ENUMS.Storage.weapons.gunmounts.AH6_M134L = 'weapons.gunmounts.{AH6_M134L}'
|
||||
@@ -1881,6 +1903,11 @@ ENUMS.Storage.weapons.gunmounts.AH_6_FN_HMP400 = 'weapons.gunmounts.{AH-6_FN_HMP
|
||||
ENUMS.Storage.weapons.gunmounts.AH_6_M134L = 'weapons.gunmounts.AH-6_M134L'
|
||||
ENUMS.Storage.weapons.gunmounts.AH_6_M134R = 'weapons.gunmounts.AH-6_M134R'
|
||||
ENUMS.Storage.weapons.gunmounts.AH_6_HMP400 = 'weapons.gunmounts.AH-6_HMP400'
|
||||
ENUMS.Storage.weapons.gunmounts.AKAN_NO_TRC = 'weapons.gunmounts.{AKAN_NO_TRC}'
|
||||
ENUMS.Storage.weapons.gunmounts.AKAN = 'weapons.gunmounts.{AKAN}'
|
||||
ENUMS.Storage.weapons.gunmounts.CHAP_HMP400LC = 'weapons.gunmounts.{CHAP_HMP400LC}'
|
||||
ENUMS.Storage.weapons.gunmounts.HMP400LC = 'weapons.gunmounts.HMP400LC'
|
||||
ENUMS.Storage.weapons.gunmounts.SHVAK_GUN = 'weapons.gunmounts.SHVAK_GUN'
|
||||
|
||||
ENUMS.Storage.weapons.droptanks.PTB_800 = 'weapons.droptanks.PTB-800'
|
||||
ENUMS.Storage.weapons.droptanks.PTB_275 = 'weapons.droptanks.PTB-275'
|
||||
@@ -1931,6 +1958,7 @@ ENUMS.Storage.weapons.droptanks.S_3_PTB = 'weapons.droptanks.S-3-PTB'
|
||||
ENUMS.Storage.weapons.droptanks.fuel_tank_370gal = 'weapons.droptanks.fuel_tank_370gal'
|
||||
ENUMS.Storage.weapons.droptanks.MIG_23_PTB = 'weapons.droptanks.MIG-23-PTB'
|
||||
ENUMS.Storage.weapons.droptanks.f_18c_ptb = 'weapons.droptanks.f-18c-ptb'
|
||||
ENUMS.Storage.weapons.droptanks.CHAP_TigerUHT_fueltank = 'weapons.droptanks.CHAP_TigerUHT_fueltank'
|
||||
|
||||
ENUMS.Storage.weapons.containers.FN_HMP400_100 = 'weapons.containers.{FN_HMP400_100}'
|
||||
ENUMS.Storage.weapons.containers.AN_M3 = 'weapons.containers.{AN-M3}'
|
||||
@@ -2062,6 +2090,13 @@ ENUMS.Storage.weapons.containers.GIAT_M621_AP = 'weapons.containers.{GIAT_M621_A
|
||||
ENUMS.Storage.weapons.containers.M134_R = 'weapons.containers.M134_R'
|
||||
ENUMS.Storage.weapons.containers.OH58D_M3P_L300 = 'weapons.containers.OH58D_M3P_L300'
|
||||
ENUMS.Storage.weapons.containers.AH6_M134R = 'weapons.containers.{AH6_M134R}'
|
||||
ENUMS.Storage.weapons.containers.CHAP_HMP400LC = 'weapons.containers.{CHAP_HMP400LC}'
|
||||
|
||||
ENUMS.Storage.weapons.missiles.C701_AShM = 'weapons.missiles.C701 AShM'
|
||||
ENUMS.Storage.weapons.missiles.IGLA_9M39 = 'weapons.missiles.9M39_IGLA'
|
||||
ENUMS.Storage.weapons.missiles.CHAP_AIM92 = 'weapons.missiles.CHAP_AIM92'
|
||||
ENUMS.Storage.weapons.missiles.AM39 = 'weapons.missiles.AM39'
|
||||
ENUMS.Storage.weapons.missiles.Shahed136_LM = 'weapons.missiles.Shahed136_LM'
|
||||
|
||||
ENUMS.Storage.weapons.torpedoes.G7A_T1 = 'weapons.torpedoes.G7A_T1'
|
||||
|
||||
@@ -2091,6 +2126,56 @@ ENUMS.Storage.weapons.containers.UH60_GAU19_RIGHT = 'weapons.containers.{UH60_GA
|
||||
ENUMS.Storage.weapons.containers.UH60_M230_RIGHT = 'weapons.containers.{UH60_M230_RIGHT}'
|
||||
ENUMS.Storage.weapons.containers.UH60L_M2_GUNNER = 'weapons.containers.{UH60L_M2_GUNNER}'
|
||||
|
||||
|
||||
ENUMS.Storage.weapons.adapters.GD_F100_SUU_13 = 'weapons.adapters.GD_F100_SUU-13'
|
||||
ENUMS.Storage.weapons.containers.GPU_5_POD = 'weapons.containers.{GPU_5_POD}'
|
||||
ENUMS.Storage.weapons.bombs.BOLT_117 = 'weapons.bombs.BOLT-117'
|
||||
ENUMS.Storage.weapons.shells.F100_M39_20_TP_T = 'weapons.shells.F100_M39_20_TP_T'
|
||||
ENUMS.Storage.weapons.gunmounts.M_39_F_100 = 'weapons.gunmounts.M-39 F-100'
|
||||
ENUMS.Storage.weapons.droptanks.GD_F100_TANK_275_L = 'weapons.droptanks.GD_F100_TANK_275_L'
|
||||
ENUMS.Storage.weapons.bombs.BLU_4B_R_GROUP_R = 'weapons.bombs.BLU-4B_R_GROUP_R'
|
||||
ENUMS.Storage.weapons.containers.F100_CAMERA_P2 = 'weapons.containers.F100_CAMERA_P2'
|
||||
ENUMS.Storage.weapons.containers.HB_ORD_SUU_7 = 'weapons.containers.HB_ORD_SUU_7'
|
||||
ENUMS.Storage.weapons.adapters.SidewinderPylon = 'weapons.adapters.SidewinderPylon'
|
||||
ENUMS.Storage.weapons.adapters.apu_470 = 'weapons.adapters.apu_470'
|
||||
ENUMS.Storage.weapons.droptanks.GD_F100_TANK_335_R = 'weapons.droptanks.GD_F100_TANK_335_R'
|
||||
ENUMS.Storage.weapons.bombs.BLU_3B_R_GROUP_R = 'weapons.bombs.BLU-3B_R_GROUP_R'
|
||||
ENUMS.Storage.weapons.droptanks.GD_F100_TANK_335_L = 'weapons.droptanks.GD_F100_TANK_335_L'
|
||||
ENUMS.Storage.weapons.adapters.mbdz_u2t = 'weapons.adapters.mbdz_u2t'
|
||||
ENUMS.Storage.weapons.containers.ZellRocket = 'weapons.containers.ZellRocket'
|
||||
ENUMS.Storage.weapons.containers.apu_73 = 'weapons.containers.apu-73'
|
||||
ENUMS.Storage.weapons.gunmounts.GAU_13 = 'weapons.gunmounts.GAU-13'
|
||||
ENUMS.Storage.weapons.shells.F100_M39_20_TP = 'weapons.shells.F100_M39_20_TP'
|
||||
ENUMS.Storage.weapons.shells.F100_M39_20_API = 'weapons.shells.F100_M39_20_API'
|
||||
ENUMS.Storage.weapons.adapters.bdz_umk2b = 'weapons.adapters.bdz_umk2b'
|
||||
ENUMS.Storage.weapons.droptanks.GD_F100_TANK_450 = 'weapons.droptanks.GD_F100_TANK_450'
|
||||
ENUMS.Storage.weapons.droptanks.GD_F100_TANK_275_R = 'weapons.droptanks.GD_F100_TANK_275_R'
|
||||
ENUMS.Storage.weapons.bombs.BLU_18_B_GROUP = 'weapons.bombs.BLU-18/B_GROUP'
|
||||
ENUMS.Storage.weapons.containers.ZELL_Booster = 'weapons.containers.ZELL Booster'
|
||||
ENUMS.Storage.weapons.adapters.apu_60 = 'weapons.adapters.apu_60'
|
||||
ENUMS.Storage.weapons.gunmounts.GPU_5_POD = 'weapons.gunmounts.{GPU_5_POD}'
|
||||
ENUMS.Storage.weapons.containers.F100_CAMERA_KA71A = 'weapons.containers.F100_CAMERA_KA71A'
|
||||
ENUMS.Storage.weapons.bombs.BLU_3_R_GROUP_R = 'weapons.bombs.BLU-3_R_GROUP_R'
|
||||
ENUMS.Storage.weapons.shells.F100_M39_20_HEI = 'weapons.shells.F100_M39_20_HEI'
|
||||
ENUMS.Storage.weapons.droptanks.GD_F100_TANK_200 = 'weapons.droptanks.GD_F100_TANK_200'
|
||||
ENUMS.Storage.weapons.adapters.apu_73 = 'weapons.adapters.apu-73'
|
||||
ENUMS.Storage.weapons.gunmounts.AKAN_NO_TRC = 'weapons.gunmounts.AKAN_NO_TRC'
|
||||
ENUMS.Storage.weapons.gunmounts.AKAN = 'weapons.gunmounts.AKAN'
|
||||
ENUMS.Storage.weapons.shells.F100_M39_20_HEI_T = 'weapons.shells.F100_M39_20_HEI_T'
|
||||
ENUMS.Storage.weapons.shells.F100_M39_20_API_T = 'weapons.shells.F100_M39_20_API_T'
|
||||
ENUMS.Storage.weapons.containers.GD_F100_STRIKE_CAMERA = 'weapons.containers.GD_F100_STRIKE_CAMERA'
|
||||
ENUMS.Storage.weapons.bombs.BLU_49_B = 'weapons.bombs.BLU-49/B'
|
||||
|
||||
ENUMS.Storage.weapons.bombs.GBU_24E = 'weapons.bombs.GBU_24E'
|
||||
ENUMS.Storage.weapons.containers.GD_F100_SUU_13 = 'weapons.containers.GD_F100_SUU-13'
|
||||
ENUMS.Storage.weapons.gunmounts.AKAN = 'weapons.gunmounts.{AKAN}'
|
||||
ENUMS.Storage.weapons.gunmounts.AKAN_NO_TRC = 'weapons.gunmounts.{AKAN_NO_TRC}'
|
||||
ENUMS.Storage.weapons.gunmounts.SUU_23_POD_Wing = 'weapons.gunmounts.{SUU_23_POD_Wing}'
|
||||
ENUMS.Storage.weapons.containers.SUU_23_POD_Wing = 'weapons.containers.{SUU_23_POD_Wing}'
|
||||
ENUMS.Storage.weapons.containers.SUU_23_POD_Centerline = 'weapons.containers.{SUU_23_POD_Centerline}'
|
||||
ENUMS.Storage.weapons.gunmounts.SUU_23_POD_Centerline = 'weapons.gunmounts.{SUU_23_POD_Centerline}'
|
||||
ENUMS.Storage.weapons.containers.HB_F_4E_BRU_42 = 'weapons.containers.HB_F-4E_BRU-42'
|
||||
|
||||
---
|
||||
-- @type ENUMS.FARPType
|
||||
-- @field #string FARP
|
||||
|
||||
@@ -753,8 +753,8 @@ UTILS.KnotsToKmph = function(knots)
|
||||
return knots * 1.852
|
||||
end
|
||||
|
||||
UTILS.KmphToKnots = function(kmph)
|
||||
return kmph / 1.852
|
||||
UTILS.KmphToKnots = function(knots)
|
||||
return knots / 1.852
|
||||
end
|
||||
|
||||
UTILS.KmphToMps = function( kmph )
|
||||
|
||||
@@ -1963,6 +1963,25 @@ function AIRBASE:Register(AirbaseName)
|
||||
|
||||
-- Category.
|
||||
self.category=self.descriptors and self.descriptors.category or Airbase.Category.AIRDROME
|
||||
|
||||
-- Get DCS object.
|
||||
local airbase=self:GetDCSObject()
|
||||
|
||||
if airbase then
|
||||
self.objectcategory=Object.getCategory(airbase)
|
||||
else
|
||||
self.objectcategory=Object.Category.BASE
|
||||
end
|
||||
|
||||
if self.objectcategory==Object.Category.BASE then
|
||||
self.objectcategoryName="BASE"
|
||||
elseif self.objectcategory==Object.Category.STATIC then
|
||||
self.objectcategoryName="STATIC"
|
||||
elseif self.objectcategory==Object.Category.UNIT then
|
||||
self.objectcategoryName="UNIT"
|
||||
else
|
||||
self.objectcategoryName="OTHER"
|
||||
end
|
||||
|
||||
-- H2 is bugged
|
||||
--if self.AirbaseName == "H4" and self.descriptors == nil then
|
||||
@@ -1971,24 +1990,24 @@ function AIRBASE:Register(AirbaseName)
|
||||
--end
|
||||
|
||||
-- Set category.
|
||||
if self.category==Airbase.Category.AIRDROME then
|
||||
self.isAirdrome=true
|
||||
elseif self.category==Airbase.Category.HELIPAD or self.descriptors.typeName=="FARP_SINGLE_01" then
|
||||
self.isHelipad=true
|
||||
self.category=Airbase.Category.HELIPAD
|
||||
elseif self.category==Airbase.Category.SHIP then
|
||||
self.isShip=true
|
||||
-- DCS bug: Oil rigs and gas platforms have category=2 (ship). Also they cannot be retrieved by coalition.getStaticObjects()
|
||||
if self.descriptors.typeName=="Oil rig" or self.descriptors.typeName=="Ga" then
|
||||
if self.category==Airbase.Category.AIRDROME then
|
||||
self.isAirdrome=true
|
||||
elseif self.category==Airbase.Category.HELIPAD or self.descriptors.typeName=="FARP_SINGLE_01" then
|
||||
self.isHelipad=true
|
||||
self.isShip=false
|
||||
self.category=Airbase.Category.HELIPAD
|
||||
_DATABASE:AddStatic(AirbaseName)
|
||||
elseif self.category==Airbase.Category.SHIP then
|
||||
self.isShip=true
|
||||
-- DCS bug: Oil rigs and gas platforms have category=2 (ship). Also they cannot be retrieved by coalition.getStaticObjects()
|
||||
if self.descriptors.typeName=="Oil rig" or self.descriptors.typeName=="Ga" then
|
||||
self.isHelipad=true
|
||||
self.isShip=false
|
||||
self.category=Airbase.Category.HELIPAD
|
||||
_DATABASE:AddStatic(AirbaseName)
|
||||
end
|
||||
if self:GetTypeName() == "Zell" then self.isZell = true end
|
||||
else
|
||||
self:E("ERROR: Unknown airbase category!")
|
||||
end
|
||||
if self:GetTypeName() == "Zell" then self.isZell = true end
|
||||
else
|
||||
self:E("ERROR: Unknown airbase category!")
|
||||
end
|
||||
|
||||
-- Init Runways.
|
||||
self:_InitRunways()
|
||||
@@ -2614,7 +2633,7 @@ function AIRBASE:GetMinimumBoundingCircleFromParkingSpots(mark)
|
||||
local spots = self:GetParkingSpotsVec2s()
|
||||
if #spots == 0 then return self.AirbaseZone end
|
||||
local center, radius = UTILS.GetMinimumBoundingCircle(spots)
|
||||
self.parkingCircle = ZONE_RADIUS:New(self.AirbaseName.." ParkingCircle",center,radius+50)
|
||||
self.parkingCircle = ZONE_RADIUS:New(self.AirbaseName.." ParkingCircle", center, radius+50, true)
|
||||
if mark == true then
|
||||
self.parkingCircle:DrawZone(-1,{1,0,0},1,{0,1,0},0.2,3)
|
||||
end
|
||||
|
||||
@@ -76,10 +76,6 @@ Functional/Tiresias.lua
|
||||
Functional/Stratego.lua
|
||||
Functional/ClientWatch.lua
|
||||
Functional/Formation.lua
|
||||
Functional/RedGCIKernel.lua
|
||||
Functional/RedGCI.lua
|
||||
Functional/RedGCI2v2.lua
|
||||
Functional/RedGCIDispatcher.lua
|
||||
|
||||
Ops/Airboss.lua
|
||||
Ops/RecoveryTanker.lua
|
||||
|
||||
Reference in New Issue
Block a user