From c59b668eec00de04e06077daca61847619fea9fa Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 19 Apr 2026 15:29:40 +0200 Subject: [PATCH 1/3] =?UTF-8?q?#TARS=20-=20Correct=20optical=20view=20proj?= =?UTF-8?q?ection=20implemented=20(ellipse=20bc=20of=20the=20angle=20of=20?= =?UTF-8?q?view)=20-=20Duration=20now=20describes=20the=20number=20of=20ex?= =?UTF-8?q?positions=20your=20camera=20has=20-=20Interval=20of=20shots=20i?= =?UTF-8?q?s=20dependent=20on=20the=20speed=20of=20the=20TARS=20unit=20-?= =?UTF-8?q?=20Added=20platform=20parameters=20overlap=20(of=20shots,=20usu?= =?UTF-8?q?ally=2025%=20for=20faster=20movers,=2050%=20for=20slower=20ones?= =?UTF-8?q?),=20overlap=20is=20clamped=20by=20minimum=20interval=20?= =?UTF-8?q?=E2=80=94=20at=20high=20speeds=20the=20interval=20floor=20is=20?= =?UTF-8?q?reached=20and=20overlap=20effectively=20increases=20(and=20you?= =?UTF-8?q?=20go=20through=20shots=20really=20fast)=20-=20Fixed=20zone=20d?= =?UTF-8?q?raw=20for=20debug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Moose Development/Moose/Ops/TARS.lua | 338 +++++++++++++++++++-------- 1 file changed, 243 insertions(+), 95 deletions(-) diff --git a/Moose Development/Moose/Ops/TARS.lua b/Moose Development/Moose/Ops/TARS.lua index 260e0db11..c0a6b4bdd 100644 --- a/Moose Development/Moose/Ops/TARS.lua +++ b/Moose Development/Moose/Ops/TARS.lua @@ -32,7 +32,7 @@ -- @field #number time Mission time (seconds) when the session was last refreshed. -- @field #number category Group category at session creation. -- @field #boolean capturing `true` while the capture loop is actively running. --- @field #number duration Remaining film in seconds. +-- @field #number duration Total expositions (shots) available per sortie. -- @field #table targetList Targets detected this pass, not yet reported. `[unitName] = snap`. -- @field #number captureCount Total unique targets captured this sortie. -- @field #boolean loop `true` while the `TARS.CaptureLoop` timer is scheduled. @@ -239,13 +239,14 @@ TARS = {} -- @field #number maxRoll Maximum bank angle (degrees) — camera must be level. -- @field #number maxPitch Maximum pitch angle (degrees) — camera must be level. -- @field #number fov Camera half-angle FOV (degrees). Not used for visual-recon helis. --- @field #number duration Total film per sortie (seconds). +-- @field #number duration Remaining expositions (shots) for this sortie. -- @field #number offset Forward look-ahead (radians). -- @field #string name Human-readable label in player messages. -- @field #number minRange Detection radius (m) at `minAlt` — visual-recon helis only. -- @field #number optimalAlt Optimal AGL (m) — triggers the dual-cone model. -- @field #number optimalRange Detection radius (m) at `optimalAlt`. -- @field #number maxRange Detection radius (m) at `maxAlt`. +-- @field #number overlap Optional frame overlap 0–1 (default 0.5). Used by `_CalcInterval`. --- F10 menu registration data. -- Stored in `TARS.groundMenus[playerName]`. @@ -282,7 +283,7 @@ TARS = {} ------------------------------------------------- --- @field #string version -TARS.version = "v2.2.2" +TARS.version = "v2.3.0" --- Active locale. -- @field #string locale @@ -362,22 +363,22 @@ TARS.reconTypes = { -- @field #table parameters TARS.parameters = {} -TARS.parameters["F-4E-45MC"] = { minAlt=100, maxAlt=6096, maxRoll=10, maxPitch=15, fov=23, duration=120, offset=math.rad(60), name="RF-4E with KS-87 Forward Oblique Camera" } -TARS.parameters["MiG-21Bis"] = { minAlt=500, maxAlt=5000, maxRoll=10, maxPitch=15, fov=52, duration=140, offset=math.rad(10), name="MiG-21R with Day recce pod" } -TARS.parameters["AJS37"] = { minAlt=15, maxAlt=1524, maxRoll=10, maxPitch=15, fov=25, duration=120, offset=math.rad(10), name="SF 37" } -TARS.parameters["Mirage-F1EE"] = { minAlt=1524, maxAlt=4572, maxRoll=10, maxPitch=15, fov=20, duration=588, offset=math.rad(10), name="Mirage-F1CR with Omera 33" } -TARS.parameters["F-5E-3"] = { minAlt=762, maxAlt=7620, maxRoll=15, maxPitch=15, fov=70, duration=300, offset=math.rad(40), name="F-5E Tigereye" } -TARS.parameters["F-14A-135-GR"] = { minAlt=750, maxAlt=5000, maxRoll=10, maxPitch=20, fov=14, duration=400, offset=math.rad(45), name="F-14A TARPS KS-87D" } -TARS.parameters["F-14B"] = { minAlt=228, maxAlt=1524, maxRoll=10, maxPitch=20, fov=85, duration=80, offset=math.rad(10), name="F-14B TARPS KA-99A" } -TARS.parameters["TF-51D"] = { minAlt=250, maxAlt=5500, maxRoll=15, maxPitch=15, fov=60, duration=400, offset=math.rad(10), name="TF-51D Mustang RF-51D Photo Recon" } -TARS.parameters["P-51D"] = { minAlt=250, maxAlt=5500, maxRoll=15, maxPitch=15, fov=60, duration=400, offset=math.rad(10), name="P-51D Mustang F-6D Photo Recon" } -TARS.parameters["P-51D-30-NA"] = { minAlt=250, maxAlt=6000, maxRoll=15, maxPitch=15, fov=60, duration=400, offset=math.rad(10), name="P-51D-30 Mustang F-6D Photo Recon" } -TARS.parameters["SpitfireLFMkIX"]= { minAlt=150, maxAlt=5000, maxRoll=15, maxPitch=15, fov=55, duration=350, offset=math.rad(10), name="Spitfire LF Mk IX PR Recon" } -TARS.parameters["FW-190A8"] = { minAlt=200, maxAlt=5500, maxRoll=15, maxPitch=15, fov=60, duration=350, offset=math.rad(10), name="FW-190 A-8 Tactical Recon" } -TARS.parameters["FW-190D9"] = { minAlt=250, maxAlt=6000, maxRoll=15, maxPitch=15, fov=60, duration=350, offset=math.rad(10), name="FW-190 D-9 Tactical Recon" } -TARS.parameters["SA342M"] = { minAlt=20, maxAlt=1000, maxRoll=35, maxPitch=25, fov=18, duration=350, offset=math.rad(10), name="SA342M EO/IR LIGHT RECO" } -TARS.parameters["SA342L"] = { minAlt=20, maxAlt=1000, maxRoll=35, maxPitch=25, fov=18, duration=350, offset=math.rad(10), name="SA342L EO/IR LIGHT RECO" } -TARS.parameters["OH58D"] = { minAlt=30, maxAlt=1200, maxRoll=35, maxPitch=25, fov=12, duration=350, offset=math.rad(12), name="OH-58D MMS EO/IR RECO" } +TARS.parameters["F-4E-45MC"] = { minAlt=100, maxAlt=6096, maxRoll=10, maxPitch=15, fov=23, duration=120, offset=math.rad(60), overlap=0.25, min_interval=3, name="RF-4E with KS-87 Forward Oblique Camera" } +TARS.parameters["MiG-21Bis"] = { minAlt=500, maxAlt=5000, maxRoll=10, maxPitch=15, fov=52, duration=140, offset=math.rad(10), overlap=0.25, min_interval=3, name="MiG-21R with Day recce pod" } +TARS.parameters["AJS37"] = { minAlt=15, maxAlt=1524, maxRoll=10, maxPitch=15, fov=25, duration=120, offset=math.rad(10), overlap=0.25, min_interval=3, name="SF 37" } +TARS.parameters["Mirage-F1EE"] = { minAlt=1524, maxAlt=4572, maxRoll=10, maxPitch=15, fov=20, duration=588, offset=math.rad(10), overlap=0.25, min_interval=3, name="Mirage-F1CR with Omera 33" } +TARS.parameters["F-5E-3"] = { minAlt=762, maxAlt=7620, maxRoll=15, maxPitch=15, fov=70, duration=300, offset=math.rad(40), overlap=0.25, min_interval=3, name="F-5E Tigereye" } +TARS.parameters["F-14A-135-GR"] = { minAlt=750, maxAlt=5000, maxRoll=10, maxPitch=20, fov=14, duration=400, offset=math.rad(45), overlap=0.25, min_interval=3, name="F-14A TARPS KS-87D" } +TARS.parameters["F-14B"] = { minAlt=228, maxAlt=1524, maxRoll=10, maxPitch=20, fov=85, duration=80, offset=math.rad(10), overlap=0.25, min_interval=3, name="F-14B TARPS KA-99A" } +TARS.parameters["TF-51D"] = { minAlt=250, maxAlt=5500, maxRoll=15, maxPitch=15, fov=60, duration=400, offset=math.rad(10), overlap=0.5, min_interval=5, name="TF-51D Mustang RF-51D Photo Recon" } +TARS.parameters["P-51D"] = { minAlt=250, maxAlt=5500, maxRoll=15, maxPitch=15, fov=60, duration=400, offset=math.rad(10), overlap=0.5, min_interval=5,name="P-51D Mustang F-6D Photo Recon" } +TARS.parameters["P-51D-30-NA"] = { minAlt=250, maxAlt=6000, maxRoll=15, maxPitch=15, fov=60, duration=400, offset=math.rad(10), overlap=0.5, min_interval=5,name="P-51D-30 Mustang F-6D Photo Recon" } +TARS.parameters["SpitfireLFMkIX"]= { minAlt=150, maxAlt=5000, maxRoll=15, maxPitch=15, fov=55, duration=350, offset=math.rad(10), overlap=0.5, min_interval=5,name="Spitfire LF Mk IX PR Recon" } +TARS.parameters["FW-190A8"] = { minAlt=200, maxAlt=5500, maxRoll=15, maxPitch=15, fov=60, duration=350, offset=math.rad(10), overlap=0.5, min_interval=5,name="FW-190 A-8 Tactical Recon" } +TARS.parameters["FW-190D9"] = { minAlt=250, maxAlt=6000, maxRoll=15, maxPitch=15, fov=60, duration=350, offset=math.rad(10), overlap=0.5, min_interval=5,name="FW-190 D-9 Tactical Recon" } +TARS.parameters["SA342M"] = { minAlt=20, maxAlt=1000, maxRoll=35, maxPitch=25, fov=18, duration=350, offset=math.rad(10), overlap=0.5, min_interval=5,name="SA342M EO/IR LIGHT RECO" } +TARS.parameters["SA342L"] = { minAlt=20, maxAlt=1000, maxRoll=35, maxPitch=25, fov=18, duration=350, offset=math.rad(10), overlap=0.5, min_interval=5,name="SA342L EO/IR LIGHT RECO" } +TARS.parameters["OH58D"] = { minAlt=30, maxAlt=1200, maxRoll=35, maxPitch=25, fov=12, duration=350, offset=math.rad(12), overlap=0.5, min_interval=5,name="OH-58D MMS EO/IR RECO" } TARS.parameters["UH-1H"] = { maxRoll=50, maxPitch=45, duration=900, offset=math.rad(6), name="UH-1H VISUAL/CREW RECO", minAlt=TARS._vAltMin, minRange=TARS._vRangeMin, optimalAlt=TARS._vAltOpti, optimalRange=TARS._vRangeOpti, maxAlt=TARS._vAltMax, maxRange=TARS._vRangeMax } TARS.parameters["Mi-8MT"] = { maxRoll=50, maxPitch=45, duration=900, offset=math.rad(6), name="Mi-8MT VISUAL/CREW RECO", minAlt=TARS._vAltMin, minRange=TARS._vRangeMin, optimalAlt=TARS._vAltOpti, optimalRange=TARS._vRangeOpti, maxAlt=TARS._vAltMax, maxRange=TARS._vRangeMax } TARS.parameters["MH-6J"] = { maxRoll=50, maxPitch=45, duration=900, offset=math.rad(6), name="MH-6J VISUAL CLOSE RECO", minAlt=TARS._vAltMin, minRange=TARS._vRangeMin, optimalAlt=TARS._vAltOpti, optimalRange=TARS._vRangeOpti, maxAlt=TARS._vAltMax, maxRange=TARS._vRangeMax } @@ -431,7 +432,7 @@ TARS.Messages = { -- ========================================================== en = { -- Capture state - TARS_FILM_START = "[TARS] Session capture activated. Film remaining: %d seconds.", + TARS_FILM_START = "[TARS] Capture activated. Expositions remaining: %d", TARS_FILM_EXHAUSTED = "[TARS] Film exhausted. Return to base for debrief.", TARS_FILM_STOP = "[TARS] Session capture ended. Return to base for debrief.", TARS_FILM_TIME_UP = "[TARS] Film time exhausted. Return to base.", @@ -439,12 +440,12 @@ TARS.Messages = { TARS_FILM_STB_MANUAL = "[TARS] <>Manual<> Film manual STB.", TARS_FILM_RESUME_MANUAL = "[TARS] <>Manual<> Film manual resume.", TARS_FILM_STB_LAND = "[TARS] <>Landing<> Film auto STB.", - TARS_FILM_RESUME_TO = "[TARS] <>TakeOff<> Film auto resume. %d seconds", + TARS_FILM_RESUME_TO = "[TARS] <>TakeOff<> Film auto resume. %d expositions", TARS_FILM_STB_LOCKED = "[TARS] Film is STB — takeoff to resume.", TARS_FILM_ALREADY_ACTIVE = "[TARS] Film already active.", TARS_FILM_NO_CAPTURE = "[TARS] No active film.", TARS_FILM_NO_CAPTURE_STOP = "[TARS] No active film to stop.", - TARS_CAPTURE_TICK = "FILM DURATION: %d seconds", + TARS_CAPTURE_TICK = "EXPOSITIONS REMAINING: %d", TARS_CAPTURE_HIT = "[TARS] +1 Captured target (%d total)", TARS_CAPTURE_HIT_MAX = "[TARS] +1 Captured target (%d total) / %d max", @@ -491,7 +492,7 @@ TARS.Messages = { -- ========================================================== de = { -- Aufnahmestatus - TARS_FILM_START = "[TARS] Aufnahme aktiviert. Verbleibender Film: %d Sekunden.", + TARS_FILM_START = "[TARS] Aufnahme aktiviert. Verbleibende Aufnahmen: %d.", TARS_FILM_EXHAUSTED = "[TARS] Film aufgebraucht. Kehren Sie zur Basis für das Briefing zurück.", TARS_FILM_STOP = "[TARS] Aufnahmesitzung beendet. Kehren Sie zur Basis zurück.", TARS_FILM_TIME_UP = "[TARS] Filmzeit abgelaufen. Kehren Sie zur Basis zurück.", @@ -499,12 +500,12 @@ TARS.Messages = { TARS_FILM_STB_MANUAL = "[TARS] <>Manuell<> Film manuell auf Standby.", TARS_FILM_RESUME_MANUAL = "[TARS] <>Manuell<> Film manuell fortgesetzt.", TARS_FILM_STB_LAND = "[TARS] <>Landung<> Film automatisch auf Standby.", - TARS_FILM_RESUME_TO = "[TARS] <>Start<> Film automatisch fortgesetzt. %d Sekunden.", + TARS_FILM_RESUME_TO = "[TARS] <>Takeoff<> Film fortgesetzt. %d Aufnahmen.", TARS_FILM_STB_LOCKED = "[TARS] Film ist auf Standby — starten Sie, um fortzufahren.", TARS_FILM_ALREADY_ACTIVE = "[TARS] Aufnahme bereits aktiv.", TARS_FILM_NO_CAPTURE = "[TARS] Keine aktive Aufnahme.", TARS_FILM_NO_CAPTURE_STOP = "[TARS] Keine aktive Aufnahme zum Stoppen.", - TARS_CAPTURE_TICK = "AUFNAHMEDAUER: %d Sekunden", + TARS_CAPTURE_TICK = "Verbleibende Aufnahmen: %d.", TARS_CAPTURE_HIT = "[TARS] +1 Ziel erfasst (%d gesamt)", TARS_CAPTURE_HIT_MAX = "[TARS] +1 Ziel erfasst (%d gesamt) / %d max", @@ -551,7 +552,7 @@ TARS.Messages = { -- ========================================================== fr = { -- État de capture - TARS_FILM_START = "[TARS] Session de capture activée. Film restant : %d seconds", + TARS_FILM_START = "[TARS] Capture activée. Expositions restantes : %d", TARS_FILM_EXHAUSTED = "[TARS] Film épuisé. Retournez à la base pour le compte-rendu.", TARS_FILM_STOP = "[TARS] Session de capture terminée. Retournez à la base.", TARS_FILM_TIME_UP = "[TARS] Temps de film épuisé. Retournez à la base.", @@ -559,12 +560,12 @@ TARS.Messages = { TARS_FILM_STB_MANUAL = "[TARS] <>Manuel<> Film en STB manuel.", TARS_FILM_RESUME_MANUAL = "[TARS] <>Manuel<> Reprise manuel du film.", TARS_FILM_STB_LAND = "[TARS] <>Atterrissage<> Film en STB automatique.", - TARS_FILM_RESUME_TO = "[TARS] <>Décollage<> Reprise automatique du film. %d seconds", + TARS_FILM_RESUME_TO = "[TARS] <>Décollage<> Film repris. %d expositions", TARS_FILM_STB_LOCKED = "[TARS] Film en STB — décollez pour reprendre.", TARS_FILM_ALREADY_ACTIVE = "[TARS] Film déjà activé.", TARS_FILM_NO_CAPTURE = "[TARS] Aucun film activé.", TARS_FILM_NO_CAPTURE_STOP = "[TARS] Aucun film actif à stopper.", - TARS_CAPTURE_TICK = "DURÉE DU FILM : %d seconds", + TARS_CAPTURE_TICK = "[TARS] EXPOSITIONS RESTANTES : %d", TARS_CAPTURE_HIT = "[TARS] +1 Cible capturée (%d au total)", TARS_CAPTURE_HIT_MAX = "[TARS] +1 Cible capturée (%d au total) / %d max", @@ -761,7 +762,9 @@ function TARS_SESSION:ReturnReconTargets() return count end ---- [INTERNAL] Starts the 10-second capture loop. +--- [INTERNAL] Starts the dynamic-interval capture loop. +-- The first tick fires after 2 seconds; subsequent ticks are spaced by +-- `_CalcInterval()` which adapts to the aircraft's current speed and altitude. -- @param #TARS_SESSION self function TARS_SESSION:CaptureData() self:T(self.lid.."CaptureData") @@ -826,15 +829,23 @@ end -- @param #TARS_SESSION self -- @param Wrapper.Unit#UNIT unit -- @param #TARS.PlatformParams params --- @return DCS#Vec2 `{ x, z }` ahead of the aircraft. -function TARS_SESSION:_OffsetCalc(unit, params) +-- @param #number center_shift shift value +-- @return #table `{ x, z, MSL, alt }` ahead of the aircraft. +function TARS_SESSION:_OffsetCalc(unit, params, center_shift) + center_shift = center_shift or 0 local pos = unit:GetPosition() local vec3 = unit:GetVec3() - local rad = math.atan2(pos.x.z, pos.x.x) + 2 * math.pi -- pos.x = Vorwärts-Vektor local MSL = land.getHeight({ x = vec3.x, y = vec3.z }) local alt = vec3.y - MSL - local dist = math.tan(params.offset) * alt - return { x = vec3.x + math.cos(rad) * dist, z = vec3.z + math.sin(rad) * dist } + local rad = math.atan2(pos.x.x, pos.x.z) -- ← unverändert + local dist = (alt / math.tan(params.offset)) + center_shift + return { + x = vec3.x + math.sin(rad) * dist, + z = vec3.z + math.cos(rad) * dist, + MSL = MSL, + alt = alt, + rad = rad, -- ← NEU: mitgeben für isInEllipse + } end --- [INTERNAL] Validates a single DCS/MOOSE object against all active filters. @@ -891,22 +902,32 @@ function TARS_SESSION:_ValidateObjectFound(_Object) return false end ---- [INTERNAL] Main capture tick. Scheduled via `timer.scheduleFunction`. +--- [INTERNAL] Main capture tick. Scheduled dynamically via `timer.scheduleFunction`. +-- The interval between ticks is calculated from the aircraft's current speed +-- and camera footprint via `_CalcInterval()`, ensuring adequate frame overlap +-- regardless of whether the platform is a slow helicopter or a fast jet. +-- `duration` is decremented by the actual interval each tick. -- @param #TARS_SESSION self The active session (timer data argument). -- @return nil function TARS_SESSION:CaptureLoop() if not self or not self.loop then return end + -- Standby: keep the timer alive but don't capture or decrement film if self.capturing and self.standby then timer.scheduleFunction(TARS_SESSION.CaptureLoop, self, timer.getTime() + 10) return end if self.capturing and self.duration > 0 then - self.duration = self.duration - 10 + -- Dynamic interval based on current speed and altitude + local params = self.Callback.parameters[self.type] + local interval = self:_CalcInterval(self.unit, params) + + self.duration = self.duration - 1 self.Callback:_MsgUnit( - self.Callback:_Txt("TARS_CAPTURE_TICK", math.max(0, self.duration)), - 9, self.playerName) + self.Callback:_Txt("TARS_CAPTURE_TICK", math.max(0, math.floor(self.duration))), + math.min(interval, 9), self.playerName,true) + self:AddToTargetList(self:FindTargets()) if self.Callback.filmLimitEnabled @@ -917,7 +938,14 @@ function TARS_SESSION:CaptureLoop() self.Callback:StopCapture(self) return end - timer.scheduleFunction(TARS_SESSION.CaptureLoop, self, timer.getTime() + 10) + + -- Debug: show interval when debugunitsearch is active + if self.debugunitsearch then + self:I(self.lid .. string.format( + "CaptureLoop interval=%.1fs duration=%.0fs", interval, self.duration)) + end + + timer.scheduleFunction(TARS_SESSION.CaptureLoop, self, timer.getTime() + interval) end if self.duration <= 0 and self.loop then @@ -947,82 +975,199 @@ function TARS_SESSION:_CalcVisualRange(params, altitude) end end +--- [INTERNAL] Calculates the capture interval in seconds based on current speed and altitude. +-- Uses the camera footprint width and the configured overlap to determine how long +-- the aircraft needs to travel before the next frame is needed. +-- Clamped between 5 and 120 seconds to prevent extreme values. +-- @param #TARS_SESSION self +-- @param Wrapper.Unit#UNIT unit The recon aircraft. +-- @param #TARS.PlatformParams params Platform parameter table. +-- @return #number interval Capture interval in seconds. +function TARS_SESSION:_CalcInterval(unit, params) + -- Current AGL from last offset calculation (reuse if available, else recalc) + local vec3 = unit:GetVec3() + local alt = vec3.y - land.getHeight({ x = vec3.x, y = vec3.z }) + alt = math.max(alt, 1) -- guard against on-ground edge case + + -- Footprint half-width b (cross-track) at current altitude + local elev = params.offset + local half_fov = math.rad(params.fov / 2) + local d_ground = alt / math.tan(elev) + local b = d_ground * math.tan(half_fov) + local diameter = 2 * b + + -- Current ground speed in m/s + local vel = unit:GetVelocityVec3() + local speed = math.sqrt(vel.x * vel.x + vel.y * vel.y + vel.z * vel.z) + speed = math.max(speed, 10) -- minimum 10 m/s to avoid division near zero + + -- Interval = footprint diameter × (1 - overlap) / speed + local overlap = params.overlap or 0.5 + local interval = (diameter * (1 - overlap)) / speed + + local min_interval = params.min_interval or 2 + return math.max(min_interval, math.min(120, interval)) +end + +--- [INTERNAL] Checks a point is inside of an elipse +-- @return #boolean IsInElipse +function TARS_SESSION.isInEllipse(ox, oz, cx, cz, a, b, heading_rad) + -- heading_rad ist jetzt direkt in Radians (aus offset.rad) + local dx = ox - cx + local dz = oz - cz + local cos_h = math.cos(heading_rad) -- ← kein math.rad() mehr + local sin_h = math.sin(heading_rad) + local lx = dx * sin_h + dz * cos_h + local ly = -dx * cos_h + dz * sin_h + return (lx/a)^2 + (ly/b)^2 <= 1 +end + --- [INTERNAL] earches for targets in a sphere ahead of the aircraft. -- @param #TARS_SESSION self -- @return #table `[unitName] = Wrapper.Unit#UNIT` +--- [INTERNAL] Searches for targets in an elliptical footprint ahead of the aircraft. +-- @param #TARS_SESSION self +-- @return #table `[unitName] = Wrapper.Unit#UNIT` function TARS_SESSION:FindTargets() local unit = self.unit - local vec3 = unit:GetVec3() - --local MSL = land.getHeight({ x = vec3.x, y = vec3.z }) - --local alt = vec3.y - MSL - local alt = unit:GetAltitude(true) - local params = self.Callback.parameters[self.type] -- #TARS.parameters + local params = self.Callback.parameters[self.type] + -- Attitude check local roll = math.abs(TARS.getRoll(unit)) local pitch = math.abs(TARS.getPitch(unit)) local isFlat = roll < params.maxRoll and pitch < params.maxPitch - local radius = params.optimalAlt - and self:_CalcVisualRange(params, alt) - or alt * math.tan(math.rad(params.fov / 2)) + -- Ellipsen-Geometrie berechnen + local elev = params.offset -- Radians, Blickwinkel nach vorne + local half_fov = math.rad(params.fov / 2) - local offset = self:_OffsetCalc(unit, params) - local coordinate = self.coordinate or COORDINATE:New(offset.x, MSL, offset.z) - coordinate = coordinate:UpdateFromVec3({ x=offset.x, y=MSL, z=offset.z }) - self.coordinate = coordinate + local center_shift = 0 + local a, b + + -- Offset berechnen (gibt jetzt MSL und alt mit zurück) + local offset_data = self:_OffsetCalc(unit, params, 0) -- erst ohne shift + local alt = offset_data.alt + local MSL = offset_data.MSL + + -- Querachse b + local d_ground = alt / math.tan(elev) + b = d_ground * math.tan(half_fov) + + -- Längsachse a + center_shift, nur wenn geometrisch gültig + if (elev - half_fov) > math.rad(2) then + local d_near = alt / math.tan(elev + half_fov) + local d_far = alt / math.tan(elev - half_fov) + a = (d_far - d_near) / 2 + center_shift = (d_far + d_near) / 2 - d_ground + else + a = b -- Fallback: Kreis + center_shift = 0 + self:I(self.lid .. "FindTargets: elev-fov margin too small, fallback to circle") + end + + -- Offset-Punkt mit korrektem center_shift + local offset = self:_OffsetCalc(unit, params, center_shift) + + -- Nach: local offset = self:_OffsetCalc(unit, params, center_shift) + local unit_pos = unit:GetVec3() + local hdg = unit:GetHeading() + -- Erwarteter Offset-Punkt: Flugzeug + (d_ground+shift) in Flugrichtung + local expected_x = unit_pos.x + math.sin(math.rad(hdg)) * (d_ground + center_shift) + local expected_z = unit_pos.z + math.cos(math.rad(hdg)) * (d_ground + center_shift) + self:I(string.format( + "OFFSET DRIFT: _OffsetCalc=(%.0f,%.0f) expected=(%.0f,%.0f) drift=(Δx=%.0f,Δz=%.0f)", + offset.x, offset.z, + expected_x, expected_z, + offset.x - expected_x, + offset.z - expected_z)) + -- Koordinate aktualisieren + local coordinate = self.coordinate + or COORDINATE:New(offset.x, offset.MSL, offset.z) + coordinate = coordinate:UpdateFromVec3({ x = offset.x, y = offset.MSL, z = offset.z }) + self.coordinate = coordinate + + -- Scan-Radius = längere Halbachse, deckt die Ellipse vollständig ab + local scan_radius = math.max(a, b) + + -- Heading für Ellipsen-Rotation + local heading = unit:GetHeading() + + -- Debug-Visualisierung if self.debugunitsearch then - -- TODO Search Zone Debug - -- Draw the searczone on the F10 map - local searchzone = self.searchzone or ZONE_RADIUS:New("TARS Debug",coordinate:GetVec2(),radius,true) - - if searchzone then - searchzone:UndrawZone() - searchzone:UpdateFromVec2(coordinate:GetVec2(),radius) - self.searchzone = searchzone - searchzone:DrawZone(-1,{0,0,1},1,{0,1,0},.2,2,true) - end - - --- TODO Params check and output - if params and unit and unit:IsAlive() then - self:I({Roll=roll,Pitch=pitch,AGL=alt}) - if roll > params.maxRoll then - MESSAGE:New(string.format("Roll - NOK out of parameters (%d°)!",roll or 0),9,"PARAM"):ToUnit(unit) - elseif pitch > params.maxPitch then - MESSAGE:New(string.format("Pitch - NOK out of parameters (%d°)!", pitch or 0),9,"PARAM"):ToUnit(unit) - elseif alt < params.minAlt or alt > params.maxAlt then - MESSAGE:New(string.format("AGL - NOK too high or too low (%dm)!", alt or 0),9,"PARAM"):ToUnit(unit) - else - MESSAGE:New("Params - OK!",9,"PARAM"):ToUnit(unit) - end - end - - end - - local debugunitset - if self.debug == true then - self:I(self.lid.."FindTargets Debug SET_UNIT created") - debugunitset = SET_UNIT:New():FilterCategories("ground"):FilterCoalitions("red"):FilterOnce() - end - - local ScannedUnits = self.Callback.detectUnits and coordinate:ScanUnits(radius) or nil - local ScannedStatics = self.Callback.detectStatics and coordinate:ScanStatics(radius) or nil + local searchzone = self.searchzone -- Core.Zone#ZONE_BASE + or ZONE_RADIUS:New("TARS Debug", coordinate:GetVec2(), scan_radius) + --if searchzone then + --if searchzone.DrawID then searchzone:UndrawZone() end + searchzone:UpdateFromVec2(coordinate:GetVec2(), scan_radius) + searchzone:DrawZone(-1, {0, 0, 1}, 1, {0, 1, 0}, .2, 2, true) + self.searchzone = searchzone + --end + + self:I({ Roll = roll, Pitch = pitch, AGL = alt, a = a, b = b, shift = center_shift }) + + if roll > params.maxRoll then + MESSAGE:New(string.format("Roll - NOK out of parameters (%d°)!", roll), 9, "PARAM"):ToUnit(unit) + elseif pitch > params.maxPitch then + MESSAGE:New(string.format("Pitch - NOK out of parameters (%d°)!", pitch), 9, "PARAM"):ToUnit(unit) + elseif alt < params.minAlt or alt > params.maxAlt then + MESSAGE:New(string.format("AGL - NOK too high or too low (%dm)!", alt), 9, "PARAM"):ToUnit(unit) + else + MESSAGE:New(string.format("Params OK | a=%.0fm b=%.0fm shift=%.0fm", a, b, center_shift), 9, "PARAM"):ToUnit(unit) + end + end + + -- Scan + local ScannedUnits = self.Callback.detectUnits and coordinate:ScanUnits(scan_radius) or nil + local ScannedStatics = self.Callback.detectStatics and coordinate:ScanStatics(scan_radius) or nil + + -- Ziele filtern local targetList = {} if alt > params.minAlt and alt < params.maxAlt and isFlat then - for _, u in pairs(ScannedUnits and ScannedUnits.Set or {}) do - if self:_ValidateObjectFound(u) then targetList[u:GetName()] = u end + + for _, u in pairs(ScannedUnits and ScannedUnits.Set or {}) do + if self:_ValidateObjectFound(u) then + local uv = u:GetVec3() + local dx = uv.x - offset.x + local dz = uv.z - offset.z + local cos_h = math.cos(offset.rad) -- ← offset.rad statt math.rad(heading) + local sin_h = math.sin(offset.rad) + local lx = dx * sin_h + dz * cos_h + local ly = -dx * cos_h + dz * sin_h + local ellipse_val = (lx/a)^2 + (ly/b)^2 + self:I(string.format( + "ELLIPSE CHECK '%s': dx=%.0f dz=%.0f lx=%.0f ly=%.0f val=%.2f %s", + u:GetName(), dx, dz, lx, ly, ellipse_val, + ellipse_val <= 1 and "HIT ✓" or "MISS (außerhalb)")) + if ellipse_val <= 1 then -- ← direkt val nutzen, kein zweiter isInEllipse-Call + targetList[u:GetName()] = u + end + end end + for _, s in pairs(ScannedStatics and ScannedStatics.Set or {}) do - if self:_ValidateObjectFound(s) then targetList[s:GetName()] = s end + if self:_ValidateObjectFound(s) then + local sv = s:GetVec3() + if TARS_SESSION.isInEllipse(sv.x, sv.z, offset.x, offset.z, a, b, offset.rad) then + targetList[s:GetName()] = s + end + end + end + + end + + -- Debug SET_UNIT (nur wenn debug flag gesetzt) + if self.debug == true then + self:I(self.lid .. "FindTargets Debug SET_UNIT created") + local debugunitset = SET_UNIT:New():FilterCategories("ground"):FilterCoalitions("red"):FilterOnce() + for _, u in pairs(debugunitset and debugunitset.Set or {}) do + if self:_ValidateObjectFound(u) then + targetList[u:GetName()] = u + end end end - -- TODO Debug - if debugunitset then - for _, u in pairs(debugunitset and debugunitset.Set or {}) do - if self:_ValidateObjectFound(u) then targetList[u:GetName()] = u end - end - end + return targetList end @@ -1074,6 +1219,9 @@ function TARS:_MsgUnit(text, seconds, playerName, Silent) if unit then MESSAGE:New(text, seconds, "TARS"):ToUnit(unit) end + if self.debug == true then + MESSAGE:New(text, seconds, "TARS"):ToAll() + end if unit and self.SRS and (not Silent) then local srsText = string.gsub(text, "^%[TARS%] ?", playerName .. ", ") srsText = string.gsub(srsText, "[<>]", "") From aff1d3c7db751f8bdfe362649c5960ff2655bbf2 Mon Sep 17 00:00:00 2001 From: leka1986 <83298840+leka1986@users.noreply.github.com> Date: Sun, 19 Apr 2026 20:35:20 +0200 Subject: [PATCH 2/3] Update Spawn.lua SPAWN:SpawnAtAirbase() and SPAWN:ParkAircraft() now handle HELIPAD/FARP parking the same way as airbases: explicit parking is honored first, helicopter spawns try the smarter parking search before falling back to the simpler free-spot table. --- Moose Development/Moose/Core/Spawn.lua | 100 +++++++++++++++++++------ 1 file changed, 79 insertions(+), 21 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 02c0cf478..a7f0dff23 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -2178,15 +2178,43 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT local scanscenery = false local verysafe = false + -- Use exact parking data when provided, otherwise let helicopters on ships/FARPs + -- use the smarter parking search before falling back to the procedural queue path. + local useexplicitspots = false + -- Number of free parking spots at the airbase. - if autoparking then + if Parkingdata~=nil then + -- Parking data explicitly set by user as input parameter. + nfree = #Parkingdata + spots = Parkingdata + useexplicitspots = true + elseif autoparking and AirbaseCategory == Airbase.Category.HELIPAD and ishelo then + if termtype == nil then + -- Helo is spawned. Try exclusive helo spots first. + spots = SpawnAirbase:FindFreeParkingSpotForAircraft( group, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, nil ) + nfree = #spots + if nfree < nunits then + -- Not enough helo ports. Let's try also other terminal types. + spots = SpawnAirbase:FindFreeParkingSpotForAircraft( group, AIRBASE.TerminalType.HelicopterUsable, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, nil ) + nfree = #spots + end + else + -- Terminal type explicitly given. + spots = SpawnAirbase:FindFreeParkingSpotForAircraft( group, termtype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, nil ) + nfree = #spots + end + + if nfree >= nunits then + useexplicitspots = true + else + -- These places work procedural and have some kind of build in queue ==> Less effort. + nfree = SpawnAirbase:GetFreeParkingSpotsNumber( termtype, true ) + spots = SpawnAirbase:GetFreeParkingSpotsTable( termtype, true ) + end + elseif autoparking then -- These places work procedural and have some kind of build in queue ==> Less effort. nfree = SpawnAirbase:GetFreeParkingSpotsNumber( termtype, true ) spots = SpawnAirbase:GetFreeParkingSpotsTable( termtype, true ) - elseif Parkingdata~=nil then - -- Parking data explicitly set by user as input parameter. (This was commented out for some unknown reason. But I need it this way.) - nfree=#Parkingdata - spots=Parkingdata else if ishelo then if termtype == nil then @@ -2238,7 +2266,7 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT local _notenough = false -- Need to differentiate some cases again. - if autoparking then + if autoparking and not useexplicitspots then -- On free spot required in these cases. if nfree >= 1 then @@ -2582,21 +2610,51 @@ function SPAWN:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex local scanscenery = false local verysafe = false - -- Number of free parking spots at the airbase. - if spawnonship or spawnonfarp or spawnonrunway then - -- These places work procedural and have some kind of build in queue ==> Less effort. - --self:T2( string.format( "Group %s is spawned on farp/ship/runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName() ) ) - nfree = SpawnAirbase:GetFreeParkingSpotsNumber( termtype, true ) - spots = SpawnAirbase:GetFreeParkingSpotsTable( termtype, true ) - --[[ - elseif Parkingdata~=nil then - -- Parking data explicitly set by user as input parameter. - nfree=#Parkingdata - spots=Parkingdata - ]] - else - if ishelo then + -- Use exact parking data when provided, otherwise let helicopters on FARPs + -- use the smarter parking search before falling back to the procedural queue path. + local useexplicitspots = false + + -- Number of free parking spots at the airbase. + if Parkingdata~=nil then + -- Parking data explicitly set by user as input parameter. + nfree = #Parkingdata + spots = Parkingdata + useexplicitspots = true + elseif spawnonfarp and ishelo then if termtype == nil then + -- Helo is spawned. Try exclusive helo spots first. + --self:T2( string.format( "Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.HelicopterOnly ) ) + spots = SpawnAirbase:FindFreeParkingSpotForAircraft( TemplateGroup, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, nil ) + nfree = #spots + if nfree < nunits then + -- Not enough helo ports. Let's try also other terminal types. + --self:T2( string.format( "Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.HelicopterUsable ) ) + spots = SpawnAirbase:FindFreeParkingSpotForAircraft( TemplateGroup, AIRBASE.TerminalType.HelicopterUsable, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, nil ) + nfree = #spots + end + else + -- Terminal type explicitly given. + --self:T2( string.format( "Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), termtype ) ) + spots = SpawnAirbase:FindFreeParkingSpotForAircraft( TemplateGroup, termtype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, nil ) + nfree = #spots + end + + if nfree >= nunits then + useexplicitspots = true + else + -- These places work procedural and have some kind of build in queue ==> Less effort. + --self:T2( string.format( "Group %s is spawned on farp %s using procedural spots.", self.SpawnTemplatePrefix, SpawnAirbase:GetName() ) ) + nfree = SpawnAirbase:GetFreeParkingSpotsNumber( termtype, true ) + spots = SpawnAirbase:GetFreeParkingSpotsTable( termtype, true ) + end + elseif spawnonship or spawnonfarp or spawnonrunway then + -- These places work procedural and have some kind of build in queue ==> Less effort. + --self:T2( string.format( "Group %s is spawned on farp/ship/runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName() ) ) + nfree = SpawnAirbase:GetFreeParkingSpotsNumber( termtype, true ) + spots = SpawnAirbase:GetFreeParkingSpotsTable( termtype, true ) + else + if ishelo then + if termtype == nil then -- Helo is spawned. Try exclusive helo spots first. --self:T2( string.format( "Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.HelicopterOnly ) ) spots = SpawnAirbase:FindFreeParkingSpotForAircraft( TemplateGroup, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata ) @@ -2659,7 +2717,7 @@ function SPAWN:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex local _notenough = false -- Need to differentiate some cases again. - if spawnonship or spawnonfarp or spawnonrunway then + if (spawnonship or spawnonfarp or spawnonrunway) and not useexplicitspots then -- On free spot required in these cases. if nfree >= 1 then From 7df1eb963f22f226d4ac679fabbc78cff544b20b Mon Sep 17 00:00:00 2001 From: leka1986 <83298840+leka1986@users.noreply.github.com> Date: Sun, 19 Apr 2026 20:36:44 +0200 Subject: [PATCH 3/3] Update CSAR.lua CSAR now treats aircraft with AircraftType[type] <= 0 as not CSAR-capable when building the active SAR unit list, while still allowing those type keys to exist for other systems such as FARP storage setup. Added those to the type, CSAR.AircraftType["Ka-50_3"] = 0 CSAR.AircraftType["Ka-50"] = 0 CSAR.AircraftType["AV8BNA"] = 0 --- Moose Development/Moose/Ops/CSAR.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index 0be8b7478..ff42181d9 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -504,6 +504,9 @@ CSAR.AircraftType["OH58D"] = 2 CSAR.AircraftType["CH-47Fbl1"] = 31 CSAR.AircraftType["AH-6J"] = 2 CSAR.AircraftType["MH-6J"] = 2 +CSAR.AircraftType["Ka-50_3"] = 0 +CSAR.AircraftType["Ka-50"] = 0 +CSAR.AircraftType["AV8BNA"] = 0 --- CSAR class version. -- @field #string version @@ -2538,8 +2541,11 @@ function CSAR:_AddMedevacMenuItem() if _unit then --self:T("Unitname ".._unit:GetName().." IsAlive "..tostring(_unit:IsAlive()).." IsPlayer "..tostring(_unit:IsPlayer())) if _unit:IsAlive() and _unit:IsPlayer() then - local unitName = _unit:GetName() - _UnitList[unitName] = unitName + local _maxUnits = self.AircraftType[_unit:GetTypeName()] + if _maxUnits == nil or _maxUnits > 0 then + local unitName = _unit:GetName() + _UnitList[unitName] = unitName + end end -- end isAlive end -- end if _unit end -- end for