Ground rendering / pushback / livery / weather / channels — 36-commit fix set#312
Merged
Conversation
v4.3.5 XP 12.4.1 Update
fix/RT: fixed epoch handling, processes buffered a/c
v4.4.0 Airplanes.live
Adds patterns for local-only scratch files and helper scripts that should not be tracked.
Introduces a new "Ground Behavior Stability" block of compile-time constants that will drive a layered fix for the visual "dance" seen when stationary or slow-taxiing aircraft are rendered from jittery 1 Hz feed positions: heading hysteresis, per-frame heading rate limiting, stationary detection, holding suppression, hard-set ground attitude, and pushback detection thresholds. This change only declares the constants — no behaviour is wired up yet. Each value carries an inline rationale comment so the chosen threshold is justified at the point of use.
Forces rendered pitch/roll to GND_PITCH_DEG / GND_ROLL_DEG each frame whenever bOnGrnd is true, with phase exceptions for FPH_ROTATE, FPH_FLARE and FPH_TOUCH_DOWN where the nose is dynamically moving relative to the ground. Also aligns the pitch MovingParam: aircraft spawned on the ground start at GND_PITCH_DEG, and the touch-down phase walks pitch to GND_PITCH_DEG (was 0). Eliminates a class of subtle pitch/roll drift that produced visual "nose-bobbing" and micro-bank in slow-taxi and parked aircraft.
Adds two ground-only filters to LTFlightData::CalcHeading: 1. Stationary freeze: when the slot is on the ground and the derived groundspeed to its neighbour(s) is at or below GND_STATIONARY_GS_KT, reuse the predecessor's already-filtered heading instead of computing one from jittery position deltas. Requires both adjacent segments (or the only available one at deque ends) to be stationary, so a single tight cluster during taxi does not lock the heading. 2. Hysteresis: after the heading has been computed, snap it to the predecessor's value when the difference is below GND_HEADING_HYSTERESIS_DEG. Absorbs the sub-degree noise that ADS-B/MLAT feeds routinely contain even on a straight ground roll. Both filters apply only to on-ground slots; airborne logic is unchanged. Together with the SIMILAR_POS_DIST short-circuit that already existed, these stop the heading sequence at gates from swinging wildly under feed jitter.
Adds a clamp in LTAircraft::CalcAcPos that limits the per-frame change in ppos.heading() to GND_HEADING_MAX_RATE_DPS * dt when the aircraft is on the ground. The check uses HeadingDiff for signed shortest-path semantics across the 360°/0° wrap and re-syncs the heading MovingParam so subsequent frames continue from the clamped value rather than catching up in a snap. Airborne heading propagation is unchanged: in the air, the heading MovingParam is already smoothed via FLIGHT_TURN_TIME, and an extra clamp would make en-route course changes visibly lag. Together with the deque-side stationary freeze and hysteresis, this ensures that even a deliberate large heading jump at slow taxi (e.g., new "to" position with a new heading) walks smoothly to the target instead of snapping.
Adds a per-aircraft holding state machine in LTFlightData: - groundHoldingSinceTs tracks the start of the current stationary- on-ground streak (feed timestamp, not sim time). - bGroundHolding flips to true once the streak exceeds GND_HOLDING_TIMEOUT_S. In AddNewPos, once bGroundHolding is set, incoming positions that fall within GND_HOLDING_TRIVIAL_DIST_M of the latest known position and whose derived groundspeed is below GND_STATIONARY_GS_KT are dropped silently — never reaching posToAdd. Movements that exceed the trivial-distance envelope (potential pushback or taxi start) always go through and reset the holding state. This is the data-layer half of the ground "dance" fix: long-parked aircraft become rock-steady because the noisy feed updates never reach the renderer. Verbose debug logs are emitted on state transitions when GetDebugAcPos is enabled for the aircraft.
Adds GND_BEZIER_MIN_HEAD_DIFF (1°) and uses it in place of the airborne 2.5° BEZIER_MIN_HEAD_DIFF when the aircraft is on the ground. Effect: slow taxi turns of 1–2° per leg are now rendered as a Bezier curve with tangent-derived heading instead of as a polyline whose heading walks via the MovingParam fallback. Combined with the rate-limit clamp and the deque-side stationary freeze, this gives visually fluid ground turns at all sizes without making airborne course corrections enter/exit Bezier mode constantly.
Adds a state-free pushback heuristic to LTFlightData::CalcHeading. When a ground slot derived groundspeed falls between GND_STATIONARY_GS_KT and PUSHBACK_DETECT_GS_MAX_KT and the track direction is at least PUSHBACK_DETECT_HEAD_DIFF_DEG away from the previous heading, the slot heading is locked to the predecessor heading rather than rotated to match the (backward) track. The rendered aircraft then visually moves backwards while keeping its nose pointed at the gate, which is the correct pushback appearance. Detection is purely sample-based: every slot is classified on its own geometry, so no extra per-aircraft state is needed and the behaviour naturally turns off when the tug stops (next slot below stationary threshold) or when the aircraft starts forward taxi (track realigns with heading).
- GND_PITCH_DEG: 2.0 -> 0.0. The 2 deg value looked wrong on common narrow-bodies (tail-dragger appearance). 0 deg matches LiveTraffic pre-existing convention (touch-down already walked pitch to 0). - Adds temporary unconditional diagnostic logging tagged GND_DIAG_ADD, GND_DIAG_CHD, GND_DIAG_HYST, GND_DIAG_FREEZE, GND_DIAG_HOLDIN, GND_DIAG_HOLDOUT, GND_DIAG_DROP in LTFlightData::AddNewPos and CalcHeading. These fire for every on-ground feed update and heading computation regardless of GetDebugAcPos, so per-aircraft pos debug does NOT need to be enabled. Search the log for "GND_DIAG_" to isolate. To be removed once thresholds are tuned.
Diagnostic logs from AA3107 (parked, RealTraffic feed providing a
stable heading) and AA2675 (slow-taxiing, RealTraffic feed providing
no heading) showed three concrete misfits in the previous thresholds:
- Parked aircraft routinely produced derived gs of 0.75-1.05 kt at the
gate purely from feed jitter, just above the 0.5 kt stationary
threshold. Holding therefore exited and small position movements
propagated to the renderer.
- Slow-taxi heading wobbled 5-7 deg per slot due to track-from-pos-
delta on RealTraffic positions without a heading field. The 0.5 deg
hysteresis never engaged.
- A single isolated above-threshold slot ended the holding streak
immediately, requiring another full 30 s of stationary samples
before suppression resumed.
Changes:
- GND_STATIONARY_GS_KT 0.5 -> 1.5 (above gate-jitter band)
- GND_HEADING_HYSTERESIS_DEG 0.5 -> 4.0 (catches slow-taxi wobble)
- GND_HEADING_MAX_RATE_DPS 60.0 -> 12.0 (matches TAXI_TURN_TIME)
- GND_HOLDING_TRIVIAL_DIST_M 7.0 -> 15.0 (covers observed envelope)
- new GND_HOLDING_EXIT_CONSEC = 2 (consecutive non-stationary
slots needed to exit)
Holding state machine in AddNewPos updated:
- Stationary slot resets groundNonStationaryCnt to 0.
- Non-stationary slot increments the counter; only exits holding once
the counter reaches GND_HOLDING_EXIT_CONSEC.
- On exit, groundHoldingSinceTs is reset to the current ts (not 0) so
that if motion ceases again immediately, the next holding promotion
is timed from the resumption of stationarity.
When the data feed (e.g. RealTraffic) supplies an explicit heading value AND the aircraft is on the ground moving below GND_USE_FEED_HEADING_MAX_KT (10 kn), use the feed value directly and skip the position-from-track derivation entirely. The previous behaviour computed heading from atan2 over consecutive lat/lon pairs, which is wrong in two important slow-ground cases: 1. Pushback. The aircraft is moving tail-first, so the track-over- ground points OPPOSITE to the nose. atan2 produced a heading ~180 deg off, which is exactly what was observed for AA1146 and AA2980 during real pushbacks at gates: their feed heading was correctly ~30 deg / ~20 deg but the rendered nose snapped to 270 deg matching the backwards motion. 2. Parked aircraft with feed-fabricated jitter. The "motion" vector between samples is dominated by noise, so the derived heading wanders. Meanwhile the feed-reported heading is usually stable and correct. The new fast-path check runs before all other heading logic in CalcHeading: if on ground and feed heading is non-NaN and derived gs < threshold, accept the feed value and return. Above the threshold the track-derived heading is the better source (it reflects the curve the aircraft is actually flying) so we fall through to the normal logic. A new GND_DIAG_FEEDHDG diagnostic logs each trust event so we can verify the rule fires for pushback/slow-taxi traffic.
Previously, FDStaticData::airlineCode() returned call.substr(0,3) when opIcao was empty. For aircraft whose callsign is actually a tail registration (N552FX, 9K1876, 1I637, etc.) this produced garbage 3-character strings like "N55", "9K1", "1I6" which XPMP2 then tried to match as an airline. No airline ICAO code begins with a digit, so the match always failed and XPMP2 fell back to "any livery of this aircraft class" — producing visibly random liveries. The fix is a simple alpha test: the first three characters of an ICAO airline ATC callsign are always letters (AAL, RPA, QFA). If any of the first three characters is not alphabetic, return an empty string so XPMP2 does type-only matching instead. From a captured session, this affects 7 of 36 wrong-livery outcomes (~20%): N-prefix registrations of small jets, GA piston aircraft, and entries where the feed delivers a registration in the callsign field. Companion change (not in this commit, ships separately): the installed Resources/relOp.txt has been extended with North American codeshare/merger groups (Republic with American Eagle, SkyWest with United Express, Endeavor with Delta Connection, US Airways with American, Continental with United, Northwest with Delta, etc.) so regional carriers without dedicated CSL liveries fall back to their parent mainline's livery rather than a random unrelated airline. That change lives at /Users/balt/X-Plane 12/Resources/plugins/ LiveTraffic/Resources/relOp.txt (the file XPMP2 actually loads at runtime) and has a mirror in the XPMP2 submodule source at Lib/XPMP2/Resources/relOp.txt which has not been committed because it requires a separate commit inside the submodule.
The ground-pitch override in LTAircraft::CalcAcPos was snapping pitch to GND_PITCH_DEG one frame after touchdown. FPH_TOUCH_DOWN is documented as a single-cycle event; the next frame is FPH_ROLL_OUT, which the override did not previously exempt. So even though CalcFlightModel kicked off a smooth pitch.moveTo(GND_PITCH_DEG) on touchdown, the override clobbered the MovingParam's walk on the very next frame — producing a one-frame nose-slam visible to the user. Fix: add FPH_ROLL_OUT to the phase exception list. During roll-out the MovingParam now walks pitch smoothly from PITCH_FLARE down to GND_PITCH_DEG over ~1 second. Once roll-out ends and the aircraft enters FPH_TAXI, the override applies again and pitch is locked at GND_PITCH_DEG (which by then matches the MovingParam value). Roll is still forced flat in all phases including the dynamic-pitch ones — there is no scenario where a wheeled aircraft banks during rotation, flare, touchdown, or roll-out.
Landing rollout and takeoff aircraft were rendered with their nose pointing toward the NEXT slot's reported heading instead of along their actual direction of motion. When the next slot was on a turn- off taxiway (heading 326 deg) and the current slot was at end-of- runway (heading 020 deg), BezierCurve::Define used to.heading() as the end-tangent, arcing the rendered path across the runway corner. Result: aircraft visually "slides off the runway" with its nose ~50 deg off the motion vector. Fix: introduce GND_TRACK_HEADING_MIN_KT (10 kn) as the boundary between "trust the feed heading" (slow taxi / pushback / parked) and "trust the motion vector" (rollout / takeoff / fast taxi). In LTAircraft::CalcAcPos two coordinated changes: 1. At leg setup, when on ground and gs >= GND_TRACK_HEADING_MIN_KT, skip BezierCurve::Define and fall through to the linear-path branch. The linear path already targets `vec.angle` (the bearing from `from` to `to` — the actual motion direction) as the heading goal, which is what an aircraft physically does on the ground at speed: nose along the track. 2. At the half-way-through-leg checkpoint, skip the retarget to `to.heading()` under the same condition. The leg-start setup has already pointed heading at the motion vector; retargeting would restart the same Bezier-clobber problem. Once gs drops below the threshold, the retarget is allowed again so the aircraft can converge on the slot's reported orientation for turn-offs, gate manoeuvres, etc. 10 kn is deliberately the same threshold as GND_USE_FEED_HEADING_MAX_KT so the two rules form one consistent boundary: below 10 kn, the feed heading wins; at or above 10 kn, the motion vector wins. No middle ground.
A previous commit added a bGndFast guard so that at high ground speed the renderer skips the Bezier path between slots and walks heading along the motion vector. The guard used GetSpeed_kt() — the currently-rendered speed at leg-setup time — which is the speed the aircraft is COMING INTO the leg, not the speed during the leg. This produced a regression on takeoff (ADO15 observed): the leg that spans taxi-into-runway has gs entering at ~5 kt and gs leaving at ~12 kt (47 m in 7.84 s). At leg-setup the rendered speed is ~5 kt, so bGndFast was false, Bezier was set up across the 89 deg corner from taxi heading (59 deg) to runway heading (330 deg), and the rendered nose arced through ~14 deg mid-leg while the aircraft positioned onto the runway. The subsequent runway-roll legs at gs > 50 kt did engage track-heading and started walking nose toward the motion vector — but the MovingParam's walk rate (TAXI_TURN_TIME) is too slow to converge before liftoff, so the aircraft was still ~45 deg off motion direction through rotation and into climb-out. Fix: use the leg's average speed vec.speed_kn() = dist/dt instead. The taxi-to-runway leg averages 11.8 kt — above the 10 kt threshold — so track-heading engages from the moment the aircraft enters that transition leg, and the nose walks straight toward the motion vector (which is along the runway) rather than arcing through the corner. By the time the aircraft is at runway-roll speeds, the rendered heading is already aligned with the runway centerline, liftoff happens nose-aligned, and the climb-out continues nose-aligned. Same fix applied to the half-way retarget skip so the two conditions remain consistent.
LTFlightData::SnapToTaxiways inserts taxiway-node waypoints into posDeque with heading=NaN, expecting CalcHeading to fill it in later. For a parked aircraft in bGroundHolding an isolated large position jump from the feed (observed: ACA34 at YSSY, 105 m jump while the RT app showed the aircraft stationary) gets accepted — it exceeds the 15 m trivial-drop threshold — and SnapToTaxiways then synthesises a sequence of intermediate waypoints along the airport taxi graph between the original parking position and the jumped position. CalcHeading on those phantom slots sees hdg_in=NaN, fails the feed-heading-trust check, fails the stationary check (the synthetic inter-waypoint derived gs is ~7 kt), and falls through to vector- from-pos-delta. The resulting headings reflect the taxiway geometry (302° → 256° → 244° in the observed case), not the aircraft's nose direction. The renderer walks the aircraft through the phantom path and the parked aircraft visually dances along a non-existent taxi route. Fix: skip the snap loop entirely when bGroundHolding is true. The glitched position still enters posDeque, but it is now interpolated as a single linear segment (a one-time visual wobble at worst) and no waypoints with their rotating headings are inserted. When the aircraft genuinely begins to taxi, AddNewPos's GND_HOLDING_EXIT_CONSEC counter clears bGroundHolding and snap-to-taxiway resumes for subsequent slots.
When RealTraffic's weather endpoint sometimes returns a stripped-down
response — no ICAO, no METAR, QNH=1013 — for a query location whose
real weather it previously delivered correctly. Observed at YSSY: a
valid {"ICAO":"YSSY","QNH":1034,"METAR":...} response followed ~60 s
later by a {"QNH":1013, no ICAO, no METAR} response for the same
query coordinates. The 1013 is RealTraffic's standard-pressure
placeholder, not a real reading.
The previous code in PreProcessWeather accepted these placeholders
unconditionally. rtWx.QNH got overwritten from 1034 back to 1013.25,
and BaroAltToGeoAlt_ft stopped applying the local-pressure correction.
At YSSY (real QNH 1034) the correction is +560 ft per 1500 ft of baro
altitude; without it, landing aircraft render ~500 ft below true MSL
and touch down on the surrounding terrain a mile short of the runway
threshold.
Fix: in PreProcessWeather, treat a response as a placeholder and skip
the SetWeather call when ALL of:
* no ICAO/airport identifier
* no METAR text
* QNH ~ 1013.25 (within 0.5 hPa)
* we already hold a confirmed non-standard QNH
The last condition lets a genuine 1013-hPa reading still arrive as a
first weather update; we only reject placeholders when they would
overwrite a previously confirmed non-standard value.
A channel that hit too many consecutive network errors gets marked bValid=false. While invalid, LTChannel::shallRun() returns false and LTFlightDataAcMaintenance() never restarts its thread — even if the user toggles the channel's enable checkbox off and back on, because the toggle only flips bChannel[ch] and leaves bValid alone. The intended recovery path was the hidden "Restart Stopped Channels" button in Settings → Basic (which calls LTFlightDataRestartInvalidChs). Most users won't notice that button and will instead reach for the channel's own checkbox, which silently does nothing — the thread never resumes even though the UI now claims the channel is enabled. Fix: in DataRefs::SetChannelEnabled, when bEnable=true, look up the LTChannel and call SetValid(true) on any channel that is currently invalid. This makes the off-then-on toggle behave the way the user expects: a re-enable revives an invalidated channel and the next maintenance tick (~2 s later) starts its network thread. SetValid(true) also resets errCnt, so the channel gets a fresh CH_MAC_ERR_CNT budget before it could go invalid again. If the underlying failure is persistent (bad credentials, server still down, …) the channel will simply re-invalidate on the next attempt and the user gets immediate visible feedback that something is wrong with the configuration — preferable to silent inaction.
A tester reported aircraft "driving sideways" during parts of taxi routes — after leaving the gate, before starting the taxi to take off, and on parts of the taxi-to-gate after landing. Investigation showed the symptom is concentrated on TURNS, not straight segments. Root cause: the heading field that LiveTraffic trusts at slow ground speed (`GND_USE_FEED_HEADING_MAX_KT = 10 kn`) is sourced from Mode S Enhanced Surveillance (EHS), which typically only updates every ~10 s and is unavailable entirely in regions without enhanced interrogation coverage. Between EHS updates the value is held constant by the receiver. During a taxi turn at 5–10 kn the aircraft can change direction by 60° or more inside one 10 s freshness window — and the held value lags. The renderer was trusting the stale value, so the nose stayed at the pre-turn direction while the body progressed along the new direction. Fix: cross-check the feed heading against the position-derived track (bearing from the predecessor slot to this one — always "now") inside the on-ground feed-heading branch of LTFlightData::CalcHeading. Three bands gated by the new GND_FEED_TRACK_AGREE_DEG = 30° constant: |Δ| < 30° agree trust feed (straight taxi or fresh EHS) 30° ≤ |Δ| ≤ 150° disagree fall through (feed has lagged in turn) |Δ| > 150° opposite trust feed (pushback) Unchanged at fast ground speeds: above 10 kn the track-heading regime in LTAircraft::CalcAcPos already owns the rendering. Unchanged when stationary: the track is meaningless under positional jitter so feed still wins. Unchanged in pushback: the >150° band detects the reversed track explicitly. Diagnostic GND_DIAG_FEEDHDG log line is extended with the track angle and a short reason string so future regressions are easy to attribute from a Log.txt capture.
Reported symptom: aircraft taking off "jump into the air" at rotation — the rendered altitude teleports from runway level to a few hundred feet in a single frame rather than walking up gradually as the aircraft rotates and lifts off. Cause: while the aircraft is on the ground, CalcPPos's `if (bOnGrnd)` branch clamps ppos.alt_m to terrainAlt_m every frame. The very next frame after CalcFlightModel decides the aircraft is airborne (`bOnGrnd` flips from true to false, phase becomes FPH_LIFT_OFF), that clamp stops applying and ppos.alt_m takes on its raw linearly- interpolated value between the last on-ground slot and the next airborne slot. Feed samples during takeoff are often 5-15 s apart, so by the time the renderer reaches the first airborne slot the interpolation has ppos.alt_m already at 100-500 ft above the runway. The aircraft visibly teleports up to that altitude in one frame. Fix: introduce LIFTOFF_BLEND_TIME_S = 1.5 s (Constants.h) and a new liftoffBlendStartTs member on LTAircraft. CalcFlightModel records the sim time on the frame `bOnGrnd` transitions true→false. For the next 1.5 s, CalcPPos's airborne branch lerps ppos.alt_m from terrainAlt_m toward the raw interpolated value using a cubic smoothstep easing (f(t) = t² (3-2t), C¹-continuous at both ends — no kink at start or finish of the blend). After 1.5 s the blend is complete and the aircraft renders at its full interpolated altitude as normal. 1.5 s coincides roughly with the pitch.max() walk driven by FPH_ROTATE, so the visual rotation and the altitude lift play out together. The cubic smoothstep avoids the linear-ramp "track up the slope" look and gives a natural ease-in/ease-out climb-out.
RealTraffic supports 2 s polling in regular operation; the previous 8 s `RT_DRCT_DEFAULT_WAIT` floor was capping the per-aircraft sample gap at ~10-20 s even when the server's RRL response would otherwise allow faster polling. The per-response `rrl` value supplied by RealTraffic is the authoritative rate limit (see ProcessFetchedData) and is honoured directly when it is at or above this floor. The floor exists only to defend against the server returning rrl=0 (or no rrl at all), in which case we fall back to this conservative interval. Lowering it to 2 s matches RT's documented minimum. Downstream effect: per-aircraft position granularity drops from ~10-20 s gaps to ~2-4 s gaps. The Mode-S EHS heading update interval (~10 s) becomes the dominant lag source rather than the polling floor, so the EHS-staleness cross-check added in commit d861869 engages less often (feed and track will agree more often) and the SnapToTaxiways backward-routing symptom investigated in task TwinFan#24 becomes much less severe (less inter-sample distance for the synthesised path to mis-route across). No new config knob is exposed: the server-supplied RRL still drives the actual cadence, this constant is purely the safety floor.
Tester reported aircraft "taxiing backwards" for parts of the route (observed: DAL973, RPA5716 at YSSY). Root cause: LTAptSnap uses pos.heading() to decide which direction along a taxi edge to route the aircraft (TaxiEdge::startByHeading / endByHeading in LTApt.cpp, called from several sites: 734, 763, 839, 1298, 1348, 1369, 1401). The feed-supplied heading is sourced from Mode S Enhanced Surveillance (EHS), which updates only every ~10 s and lags noticeably during turns. When SnapToTaxiways processes a slot whose feed heading is stale, the synthesised taxi path between this slot and the next gets routed in the wrong direction along the edge — the rendered aircraft visibly moves backward along its taxiway between feed samples. CalcHeading (since commit d861869) already cross-checks the feed heading against the position-derived track and falls through to the track-derived value when the two disagree by 30–150°. But CalcNextPos ran SnapToTaxiways BEFORE the CalcHeading loop, so snap was reading the raw feed heading, bypassing the cross-check. Fix: in SnapToTaxiways, call CalcHeading on each on-ground slot immediately before LTAptSnap. This applies the full on-ground heading filter chain — stationary freeze, feed/track cross-check, pushback detect, hysteresis — so snap sees a heading that reflects the actual direction of travel rather than the stale EHS value. The existing post-snap CalcHeading loop in CalcNextPos stays in place. Its role is to fill in the heading of the intermediate waypoints that SnapToTaxiways itself synthesises (inserted with heading=NaN, then derived from the bearing between consecutive waypoints once the path geometry is known). The recent RT 2 s polling change (6fb0b1f) reduces but does not eliminate this symptom — even at 2 s feed samples, if the EHS lag exceeds 2 s (which can still happen) snap could route wrong. This fix addresses the underlying cause regardless of polling rate.
Replaces the linear chord interpolation between two consecutive ground
position slots with a centripetal Catmull-Rom spline fit through four
control points: P0 = the most-recently-retired `from`, P1 = current
from, P2 = current to, P3 = the slot after `to` if available. The
rendered position comes from the spline at the current leg parameter,
and the rendered heading comes from the spline's tangent at the same
parameter (atan2 of dC/dt).
Why this is the right shape of fix
The previous chord-based interpolation produced a visible "sideways"
look during taxi turns: the rendered position cut straight across each
turn corner while the rendered heading walked toward the chord
direction via a rate-limited MovingParam. Because the chord is the
average of the entry and exit headings, the nose always lagged the
actual taxiway tangent by half the per-leg heading change. With
larger feed gaps (or at higher taxi speeds) the lag became visually
objectionable — tester reports for TAP227, TAP215 and others.
A smooth curve through four positions captures the actual arc the
aircraft flew, and its tangent at any parameter is by construction
the direction of motion at that point. Position and heading then come
from the same curve and are aligned by construction — no separate
rate-limit, no chord-vs-arc mismatch.
Centripetal (Lee 2009) parameterisation is specifically chosen over
the uniform or chordal variants because it is the only Catmull-Rom
form that avoids cusps and self-intersecting loops at sharp corners,
which we routinely encounter on runway turn-offs (45-90° in 30-60 m).
Math implementation
New helper `CatmullRomEvalCentripetal` in CoordCalc.{h,cpp}. Operates
on a local meters frame centred at P1 (using existing Lat2Dist /
Lon2Dist helpers) so the math is Euclidean and avoids cos(lat)
accumulation across control points. Knot intervals use sqrt(chord
distance) per the centripetal variant; barycentric (Lee) evaluation
form for both the position and a finite-difference tangent.
Heavily commented inline.
LTAircraft changes
* New `posPrev` member (positionTy, default NaN) stores the slot
being popped during the position switch in CalcPPos so it remains
available as P0 for the next leg's spline.
* In CalcPPos, the non-Bezier branch now picks between spline and
linear:
- Both endpoints on ground → spline (lat/lon + heading from
spline; alt + pitch stay linear because they're not part of
the horizontal-plane curve and get clamped/forced later by
the bOnGrnd block anyway).
- Any other case → linear, as before. This includes cruise
legs, the touchdown / liftoff boundary, and air segments.
* Existing Bezier path (turn.GetPos) still wins when active — that
machinery is unchanged and continues to handle cut-corner setups
elsewhere in the flight model. On the ground, Bezier rarely
fires now because the per-frame heading rate limit and the new
spline both produce smooth results without it.
What this replaces / interacts with
* Per-frame heading rate limit (a1e0423): still in place, still
runs after the spline assigns ppos.heading(). It's a no-op
against spline output because the spline tangent already varies
continuously with the parameter; the clamp only ever fires for
the linear-fallback case now.
* Half-way retarget to to.heading(): unchanged — still skipped at
high ground speed; allowed at low ground speed where the slot
heading from the feed/track cross-check (d861869) is a useful
secondary target. The spline-set heading dominates in practice.
* SnapToTaxiways waypoint insertion: complementary. Snap inserts
extra control points along the taxi graph; the spline then
naturally curves through them, which is exactly the smooth
taxiway-centreline shape we want.
After the RT polling floor dropped to 2 s (commit 6fb0b1f) the log started showing curl error 55 (CURLE_SEND_ERROR) with the message "Connection died, tried 5 times before giving up", at roughly 27 s intervals. Each occurrence cost five send attempts plus a channel error counter tick. Symptom is the classic stale-HTTP-keep-alive case: the previous request's TCP connection is silently dropped by an intermediate NAT/firewall (or by the server) while we are idle between polls, and libcurl finds out only when its next `send()` on the kept-alive socket fails. At an 8 s poll cadence the idle interval was short enough that the connection rarely went stale; at 2 s it should be even less of a problem in principle, but the symptom suggests that some hop in the path declares the socket idle on a sub-30 s timer that the previous cadence happened to dodge by aligning with the server's own keepalive cadence. Fix: enable TCP keepalive at the OS level via curl options in LTOnlineChannel::InitCurl: * CURLOPT_TCP_KEEPALIVE = 1 enable SO_KEEPALIVE * CURLOPT_TCP_KEEPIDLE = 20 first probe after 20 s of idle * CURLOPT_TCP_KEEPINTVL = 10 subsequent probes every 10 s With the OS sending a TCP heartbeat on each idle socket, NATs and servers see the connection as active and don't drop it. The kept- alive HTTP connection then stays usable for the next 2 s poll, eliminating the CURLE_SEND_ERROR cluster. Applies to all online channels (OpenSky / ADSBHub / RealTraffic / etc.) since the option is set in the base-class InitCurl. The non-RT channels poll less frequently so they were unlikely to hit the symptom, but keepalive is a strict improvement for any persistent HTTPS connection that may sit idle for tens of seconds.
TCP keepalive (cee1c1c) did not stop the CURLE_SEND_ERROR / "Connection died, tried 5 times" pattern recurring at ~30s intervals. Whatever is recycling the kept-alive socket does so actively (upstream load balancer, NAT idle-drop, or curl's own connection-cache age limit), and keepalive probes cannot help against an active RST or FIN. Set CURLOPT_FORBID_REUSE so every request opens a fresh TCP+TLS connection and closes it after the response: no kept-alive socket ever survives long enough to go stale. Cost is one TLS handshake per request (~100-300ms with TLS 1.3 resumption), negligible at the 2s RT polling cadence. The keepalive options are kept as belt-and-braces.
Builds on the centripetal Catmull-Rom ground spline (486b916) with a set of fixes for jitter, speed pulsation and segment-boundary jumps surfaced during testing: - Arc-length reparameterisation: a per-segment 16-sample LUT (CatmullRomArcLut) maps the time-linear parameter onto constant arc-length-per-time, so the rendered position no longer speeds up and slows down within a leg. - Cache P3 (posNext) at segment switch instead of reading posList[2] live each frame. A mid-segment feed update no longer shifts the spline geometry underfoot and snaps the rendered position. - Control-point smoothing: pre-smooth the look-ahead endpoint P2 with a [1,2,1]/4 binomial kernel so the spline approximates rather than interpolates noisy feed samples. P1 is left raw so the curve still returns 'from' exactly at u=0, keeping segment joins seamless with the posList.front()=ppos continuity mechanism. - High-speed fallback: above 40kt (takeoff roll, landing rollout) skip the spline for plain linear interpolation. The aircraft tracks a straight line there; the spline only amplifies glitchy feed data and interacts awkwardly with the acceleration-profile parameter. - Near-stationary fallback: below a 5m chord, skip the spline so a noise-dominated tangent cannot wobble the heading of a parked aircraft. - Tolerate sub-1s backward sim-time jumps in NextCycle instead of tearing down and rebuilding the whole aircraft fleet on every frame stutter or brief pause/unpause.
The RealTraffic parked-aircraft feed was almost entirely missing in the sim. Several compounding bugs, fixed together: - Dedup collapsed empty parking-position names. ProcessParkedAcBuffer dedups by RT_PARK_ParkPosName (Jeppesen stand), keeping the newest-timestamp entry per stand. But that field is frequently empty (GA, cargo, remote stands), so every empty-name aircraft collided into one map slot and all but one were dropped. Empty names now key on the unique hex id instead, so each is kept. The log line also reports the post-dedup count. - SPOS_STARTUP was set only on a successful gate match. Without that flag an aircraft never enters FPH_PARKED, so the Synthetic channel never adopts it to keep it alive and it ages out minutes after creation; and the ground-holding trivial-drop is not exempted for it. The flag (and bHeadFixed) is now set unconditionally — RT's feed is authoritative that the aircraft is parked, gate match or not. - Ground-holding trivial-drop ate the bootstrap seeds. AddNewPos drops stationary near-duplicate positions as jitter once holding is active. The parked feed's four identical seed positions (and the Synthetic keep-alive re-feeds) are exactly that shape, so they were dropped and the aircraft was left with too few positions to render. SPOS_STARTUP positions are now exempt from the drop: they are intentional placements, not feed jitter, and raw live-feed jitter is not SPOS_STARTUP at AddNewPos time (snapping runs later). - Every parked aircraft faced north. The gate match was tested via startupPos.isNormal(), but LTAptFindStartupLoc returns the matched apt.dat location with a NaN timestamp and isNormal() rejects a NaN ts — so the match was discarded every time and the heading override never ran. The match is now tested on the returned distance (NaN only when nothing matched). The startup-location search radius is also widened to the 3x default since RT's coordinates are not metre-precise, and dyn.heading is synced after the gate-match correction. - Gate handoff was indiscriminate. SyntheticConnection removed any stored parked aircraft within 10 m of any on-ground aircraft, so traffic merely taxiing past a gate wrongly evicted parked aircraft. The trigger is now ac.GetFlightPhase() == FPH_PARKED: only an aircraft that has itself come to rest on a stand evicts a stale ghost there. - Parked traffic is now re-fetched periodically. The request was a one-shot fired only on airport-data load, freezing the parked picture for the whole visit. RT_PARKED_REFRESH_INTVL_S (300 s) re-arms it so new arrivals appear and departures are reconciled.
The parked-traffic request was gated on LTAptAvailable() at issue time (SetRequType), but everything downstream was not — ProcessParkedAcBuffer could run ~1-2s later against a half-built gmapApt after the camera moved and a new async apt.dat load started. LTAptSnap and LTAptFindStartupLoc were not gated at any point, so live arrivals could also snap against a reloading layout and pick up wrong startup locations / wrong headings that then stuck (KLM604 "facing ass to the terminal" was an example). Three gates added: - ProcessFetchedData re-checks LTAptAvailable() when the parked response arrives, before calling ProcessParkedAcBuffer. If the layout went not-ready in the request->response window, the response is dropped, bDoParkedTraffic stays armed, and tLastParkedRefresh is left unchanged so the next cycle retries promptly. - LTAptSnap bails immediately if !LTAptAvailable(). AsyncReadApt first PurgeApt's, then re-adds airports one by one, releasing the lock between each, so a snap against gmapApt mid-load lands in the wrong taxiway / startup location and the bad heading sticks. - LTAptFindStartupLoc bails similarly. ProcessParkedAcBuffer is now also gated upstream, but this also protects the Synthetic channel's call site and any future lookup that races a mid-flight reload. Direct answer to the dev's "hope it doesn't backfire" concern after the parked-traffic acceleration work.
…ete) Replaces the state-free per-slot pushback heuristic with a real state machine, restructured to run BEFORE the feed-heading block so it is authoritative once it engages. Adds the renderer bridge needed for the held heading to actually reach the screen, and chooses the heading source per slot based on what the feed is actually reporting. THIS IS NOT YET A WORKING FIX. Pushback detection still misses cases where the feed heading is course-over-ground and the parked->push geometry is borderline (DAL73), false-triggers / fights for cases where the snapping pipeline interleaves SPOS_STARTUP positions into the deque (AMX026), and does not fully reproduce the "facing ass to the terminal" path (KLM604 — separate snapping issue). Work continues; this commit captures the state to keep the Windows-build tester in sync. What changed: - LTFlightData::bPushback state (Include/LTFlightData.h). Plus headingStable / headingStartup members consumed by the PUSHBACK_DIAG diagnostic and likely by the next round of detection redesign. - PUSHBACK_DETECT_GS_MAX_KT raised 3->12 (entry-only ceiling now; the 135deg direction gate is the real discriminator). PUSHBACK_EXIT_FWD_DIFF_DEG added (45deg, exit-when-moving-forward-relative-to-maintained-nose). PUSHBACK_FEED_IS_NOSE_DEG added (90deg, switches the held-heading source between feedHdg (true-nose feeds) and track+180 (course-over- ground feeds)). PUSHBACK_MIDPOINT_DIST_M removed (was dead code). - CalcHeading: pushback state machine moved BEFORE the feed-heading cross-check so it is authoritative when engaged. ENTRY on the geometric signature, HOLD chooses feedHdg-vs-track+180 per slot based on |feedHdg-track|, EXIT on direction reversal (no timer). Sub-threshold-motion slots carry the predecessor heading. Airborne forces bPushback=false defensively. - LTAircraft spline branch: when from.f.bHeadFixed is set, the rendered heading is interpolated from the slot headings instead of overridden by the spline tangent. Without this, the held pushback heading was discarded at render time because the tangent points along the (backward) motion. - TEMPORARY: PUSHBACK_DIAG logging in CalcHeading dumps feedHdg, track, the captured stable / startup reference headings and the angular gaps to each. To be removed once the detection is sorted.
RealTraffic v6 added a hex-keyed ICAO operator/airline flag code to both the UDP RTTFC broadcast (field 43, RT App v11.1.452+) and the HTTP Direct API JSON array (field 48). It is sourced from RT's OperatorFlagCode column of the OpenSky-derived BaseStation aircraft database, keyed on the transponder hex ID rather than the operating callsign — so it remains correct under wet-lease and codeshare operations where the callsign-derived airline would be wrong. LiveTraffic now populates FDStaticData::opIcao from this field in both processing paths, bounds-checked so older RT App builds / sparser responses still parse. When present (≈55% of records in test, doc says ~70% — the rest are GA/private airframes RT does not have in its DB), it wins over the callsign-substring guess in airlineCode() and is used authoritatively for XPMP2 livery matching. When absent it falls through to the existing fallback path with no behaviour change. UDP enum (RT_RTTFC_FIELDS_TY): index 41 renamed from AUGMENTATION_STATUS to BARO_ALT_UNCORRECTED (v6 changed the slot's semantics; the old name was not referenced anywhere). Added AUTHENTICATION (=42, shares slot with the preserved MIN_TFC_FIELDS strict-parse gate) and OPERATOR (43). The min-fields gate stays at 42 so older RT App builds that do not send fields 42-43 still parse. HTTP Direct enum (RT_DRCT_FIELDS_TY): inserted RT_DRCT_Operator at index 48 ahead of the NUM_FIELDS sentinel. The existing strict gate at ProcessFetchedData picks up the new sentinel value cleanly because v6 Direct API responses always carry 49 fields per the doc. Verified in-session in both RT App (UDP) and Direct API (HTTP) modes: 840 RealTraffic adds, 461 with a populated operator code, 379 empty and falling through to the callsign-substring fallback as designed.
Two related guards to stop a previously evicted parked ghost from coming back, plus a debug channel for diagnosing position-feed issues: - SyntheticConnection: persist hex ids that gate-handoff has evicted in a session-scoped set (evictedHexIds). The Synthetic re-adoption path now skips marked ids, blocking the race where a FetchAllData pass running during the async SetInvalid teardown recreated the ghost ~40s after eviction. - ProcessParkedAcBuffer: refuses to re-seed a marked id, and as defence-in-depth refuses to re-seed any hex that is already live-tracked and has either left the ground or moved farther than GATE_REFEED_MAX_DIST_M (50 m) from the gate position RT is reporting. Blocks the TFL3NA-class symptom where the 5-minute parked re-fetch silently appended an SPOS_STARTUP gate-position seed to an airborne aircraft deque, causing the render clock to later walk into it and teleport the aircraft back to the gate. Adds GATE_REFEED_MAX_DIST_M constant in Constants.h with verbose rationale on why 50 m is the right footprint for still-at-the-stand. Also lands FEED_DIAG instrumentation across both RT paths (ProcessTrafficBuffer / HTTP-Direct and ProcessRTTFC / UDP RTTFC): per-aircraft monotonicity log marking NEW / OK / REPEAT / BACKWARDS for every accepted feed timestamp, plus the source msg_type and seen age. Drives the recent debug analyses (TFL3NA, KLM911, KLM99F, KLM48/KLM874 gate handoff) and remains intentionally tagged TEMPORARY in the source so it is easy to find and remove once the feed-jitter work settles.
Several intertwined changes that together fix the visible takeoff glitches users reported (discrete 100ft+ altitude jumps mid-climb, aircraft slamming back to runway after partial liftoff, premature rotation animation without altitude change). Smoothing the rendered climb-out altitude: - LookupAltAtTs in LTAircraft now fits altitude via a Gaussian- weighted local linear regression (sigma 5s) instead of returning the raw per-leg linear interpolation. ADS-B reports altitude in 25ft quantization steps and slots arrive at irregular 1-5s cadence, so the per-leg slope swings wildly even during a steady climb; an interpolating spline (PCHIP etc.) faithfully reproduces that as visible kinks every few seconds. The regression renders along the fitted trend instead. - Numerically stable implementation: works in (t - targetTs) so the variance and covariance sums stay in the +-30s range. The textbook Sum(w*t*t) - sumW*tMean*tMean form would subtract two values of order 3e19 (timestamps are around 1.78e9) to get a result of order 1e2, losing the signal to floating-point cancellation. - pastAltSamples_ archive: fd.posDeque only keeps one past slot at any moment (CalcNextPos pops aggressively), and a regression with near-unit weight on that one sample shifts discretely when it pops. The archive mirrors every slot we ever observe and does NOT pop, so the Gaussian weight on each sample fades out continuously over many frames as targetTs moves past it. No more step changes at the deque-pop boundary. Liftoff-blend ground lock + floor clamp in CalcFlightModel / CalcPPos: - During the 10-second liftoff blend, bOnGrnd is locked to false even if LookupAltAtTs momentarily dips below MDL_CLOSE_TO_GND above terrain. Without this guard, transient regression dips (smoothing-window rebalancing as past-ground samples weight-out against future-airborne samples) would re-flip bOnGrnd to true, clamp ppos.alt to terrain, regress the phase from FPH_LIFT_OFF to FPH_TO_ROLL, and the next frame's recovery would reset liftoffBlendStartTs and restart the blend from scratch. Visibly: aircraft climbs to ~Nft, slams to the runway, continues T/O roll, rotates again, climbs again. - The blend formula clamps (LookupAltAtTs - liftoffStartAlt) to be non-negative so the rendered altitude can never go below terrain during the blend even if the smoothed input dips below the frozen start. - ALT_DIAG log line on every bOnGrnd transition with the relevant state (PHeight, blend status, sinceLO, phase) so future debugging can verify there is one clean transition per real liftoff. RealTraffic ingest: - posTime = TimeStamp - PosAge on both HTTP-Direct and UDP RTTFC paths. The RT TimeStamp is when the record was generated server- side and PosAge is the ingest age of the underlying measurement. Different sources (ADS-B vs MLAT vs satellite multilateration) have very different ingest delays, so two consecutive records can have the same TimeStamp but represent measurements taken seconds apart. Normalising to actual measurement time aligns the deque with what-the-aircraft-did-when rather than when-RT-ingested-it. - FEED_DIAG now also includes alt, gnd flag, and reported vsi so debug analyses can see what the feed is actually saying. Takeoff/landing animation polish (carried over from earlier in the session, not yet shipped): - LIFTOFF_BLEND_TIME_S bumped from 1.5s to 10s for a gradual climb-away from the runway. With the spline / regression smoothing on top, the blend curve is C2-continuous at both endpoints and renders as a single smooth arc. - TOUCHDOWN_HOLD_PITCH_S = 5s: after FPH_TOUCH_DOWN the pitch.moveTo to GND_PITCH_DEG is deferred for 5s, modelling the aerobrake during which real airliners keep the nose up after the mains touch. Previously the nose was on the ground within ~3s of touchdown.
…e exception
Two changes addressing reported visual pitch glitches on take-off.
ROTATE_PITCH_MAX_DEG = 10 deg
- FPH_ROTATE entry used to call pitch.max(), which walks the pitch
MovingParam toward pMdl->PITCH_MAX (15 deg by default). 15 deg is
past the tail-strike geometry of most narrow-bodies (B738 ~ 11 deg,
A320 ~ 13.5 deg), and users were seeing rendered aircraft drag
their tails during rotation.
- New constant ROTATE_PITCH_MAX_DEG = 10 caps the rotation target.
pitch.moveTo(ROTATE_PITCH_MAX_DEG) replaces pitch.max() in the
ENTERED(FPH_ROTATE) block.
- After FPH_LIFT_OFF, the in-air pitch logic in
LTFlightData::CalcNextPos (around line 1700) takes over and walks
pitch toward a VSI-derived target, still clamped to
pMdl->PITCH_MAX. So steep climbs still reach the full 15 deg, just
not while the gear is on the runway.
FPH_LIFT_OFF added to the ground-attitude pitch override exception
- CalcAcPos used to force pitch back to GND_PITCH_DEG whenever
bOnGrnd was true and phase was not in
{FPH_ROTATE, FPH_FLARE, FPH_TOUCH_DOWN, FPH_ROLL_OUT}. Phase can
advance ROTATE -> LIFT_OFF via the V_Climbing branch in
CalcFlightModel as soon as VSI crosses VSI_STABLE (100 fpm) -
which happens BEFORE bOnGrnd actually flips false on takeoffs
with smoothed altitude (the regression has not yet crossed
MDL_CLOSE_TO_GND above terrain).
- With FPH_LIFT_OFF missing from the exception list, the override
forcibly reset ppos.pitch() to 2 deg for the frames between
phase-becomes-LIFT_OFF and bOnGrnd-actually-flips-false.
Visible as: nose pitches up, briefly flips level on the runway,
then pitches up again once airborne. Reported on AAL2449.
- Adding FPH_LIFT_OFF to the exception list keeps the MovingParam
in control of pitch during the entire rotate-to-airborne window.
Reverts the `posTime -= jag_n(pJAc, RT_DRCT_PosAge)` adjustment added in commit d36ba8c on both ingest paths (HTTP-Direct ProcessTrafficBuffer and UDP RTTFC ProcessRTTFC). The original adjustment was based on an incorrect interpretation of the two time-related fields in the RT data: it assumed RT_DRCT_TimeStamp (idx 10) was a server-side record-creation timestamp and RT_DRCT_PosAge (idx 39) was the ingest-pipeline latency back to the actual measurement time, so subtracting one from the other would normalise positions to "what the aircraft did when". That is not what these fields mean in RT v6: idx 10 is already the measurement time and is the only field that should be used for chronological comparisons. The subtraction was actively moving every position backwards by an amount that does not represent any real lag, breaking deque ordering — slots from sources with higher PosAge values were being placed too far in the past, manifesting as cascades of BACKWARDS rejections and deque-position artefacts in the FEED_DIAG log. posTime on both paths is now back to the raw RT_DRCT_TimeStamp / RT_RTTFC_TIMESTAMP value, exactly as it was before d36ba8c. FEED_DIAG still prints PosAge as `seen=` for informational purposes but the `ts=` field and the OK / BACKWARDS / REPEAT flags now reflect raw-timestamp deltas, which is the correct chronology to use.
Builds the full pipeline for correctly identifying parked aircraft at gates and rendering their pushback maneuver, replacing the previous heading-flip- based heuristic with a state machine driven by gate detection, distance- gated motion acceptance, and locked nose-source choice. Constants.h - GATE_DETECT_MAX_DIST_M (30 m): max distance from apt.dat startup-loc for a held position to count as "at a gate". Tight on purpose — keeps runway hold-shorts and taxi pauses from being misclassified. - GATE_HOLD_MIN_ACCEPT_M (30 m): minimum displacement before the first motion slot is accepted on a gate-parked aircraft. Distance-based, NOT counter-based, because real pushbacks roll at 0.4-1.4 kt — below the stationary threshold — and a non-stationary-slot counter never advances for a real slow push. - PB_MOTION_GS_KT (0.3): pushback state-machine bMotion threshold, distinct from the global GND_STATIONARY_GS_KT (1.5). The global threshold is too high for real slow pushbacks; the state machine needs its own. - PB_FEED_NOSE_AGREE_DEG (30): max delta between parked heading and first- motion feed heading for the feed to be treated as true-nose source. LTFlightData (CalcHeading PB block) - bMotion loosened: drop the per-slot dist>=SIMILAR_POS_DIST requirement, use PB_MOTION_GS_KT. Continuous slow motion stays in PB_ACTIVE instead of bouncing into PB_PAUSED on every sub-7m slot. - Nose source locked at entry: FEED if first-motion feedHdg is within PB_FEED_NOSE_AGREE_DEG of the prior parked heading (true-nose case), otherwise TRACK+180. Held for the entire push — no per-slot flipping. - Statistical 4-slot track filter (equal-weight centroid, reject 2 largest outliers, vector between 2 inliers). Replaces the noisy 2-point chord during slow ground motion. - Predecessor fallback: when posDeque is drained during a long stationary period and the GATE_RELEASE slot arrives alone, fall back to pAc->GetToPos() as virtual predecessor so the state machine can engage on the first accepted slot. - bHeadFixed pinning: stamp the predecessor slot (guarded against posDeque.cbegin) so the renderer interpolates across the entry leg. LTFlightData (AddNewPos) - Third gate-detection path: on bGroundHolding flip, one-shot LTAptFindStartupLoc lookup; if within GATE_DETECT_MAX_DIST_M, set bGateParked. Distance signal complements the SPOS_STARTUP path and catches live-tracked aircraft that were never in RT's parked snapshot. The lookup uses outDist (not isNormal()) as the success signal — startup-loc positionTy has ts=NaN/alt=NaN and isNormal() rejects it. - Distance-based gate-hold motion suppression: drop any slot with displacement < GATE_HOLD_MIN_ACCEPT_M from the latest accepted position while bGateParked && pbState==PB_NONE. On acceptance, also clear bGroundHolding so subsequent in-push slots flow through. - youngestTS advancement on drops: keep the freshness timestamp moving even when slots are filtered out. Without this, an aircraft sitting at a gate receiving valid feed updates (all trivial-dropped) outdates after GetAcOutdatedIntvl() and is removed despite the feed being alive. LTAircraft (ground spline branch) - Honour to.f.bHeadFixed as well as from.f.bHeadFixed when deciding whether to interpolate heading or use the spline tangent. The previous from-only check failed at the PB_NONE→PB_ACTIVE transition: from (posList[0]) was a live-feed parked slot with bHeadFixed=false, so the renderer used the motion-tangent (≈ direction of travel) and visually pivoted the aircraft to face the push direction at the gate. Honouring to.f.bHeadFixed covers the transition from the destination side, since pinning bHeadFixed retroactively on the predecessor isn't reachable when posDeque has been drained. LTMain - Cosmetic (whitespace) from earlier revert of rewind-detection feature. Diagnostic logging - GND_DIAG_GATE (HIT / NEAR / NOAPT / APT_UNAVAIL) at HOLDIN moment. - GND_DIAG_GATE_HOLD on suppressed motion at gate (with dist/threshold). - GND_DIAG_GATE_RELEASE on acceptance. - PUSHBACK_DIAG ENTRY with chosen nose source and entry values. - Per-slot PUSHBACK_DIAG (state, gs, track, heldNose, assigned).
PB safety-valve emergency exit
- New constant PB_MAX_GS_KT = 10.0 in Constants.h.
- In CalcHeading, when pbState ∈ {ACTIVE, PAUSED} and the slot gs
exceeds PB_MAX_GS_KT, force exit to PB_NONE: clear pbHeldNose,
pbUseFeedNose, bGateParked and log PUSHBACK_DIAG ... FORCE_EXIT.
- Catches the failure mode where the held-nose source was chosen
incorrectly at entry (TRACK+180 picked because feedHdg disagreed
with parkedHdg, but the feed was actually right because the
aircraft had already rotated during GATE_HOLD suppression). With a
wrong reference, the directional PB_PAUSED→PB_NONE exit test never
fires; the aircraft taxis out at 20+ kt while the renderer still
shows the held nose, producing the visible tail-first / ass-
forward symptom. 10 kt is a hard upper bound on physical pushback
speed; anything above is definitively taxi.
`FF****` placeholder-hex duplicate prune
- New static LTFlightData::PrunePlaceholderHexDuplicates() (declared
in LTFlightData.h, implemented in LTFlightData.cpp).
- Two-pass scan of mapFd: pass 1 collects callsigns from non-FF
entries; pass 2 invalidates any FF entry whose callsign matches.
Two passes are necessary because the real-hex entry may appear
after its FF counterpart in iteration order. SetInvalid(true)
drops the rendered aircraft so the visual duplicate disappears
immediately and the standard cleanup pipeline erases the entry.
Uses GetUnsafeStat() for the callsign reads — racy but acceptable
given the periodic nature of the scan (stale reads re-evaluated
next pass).
- Hooked from LTRegularUpdates with a 10 s throttle; the duplicate
condition develops over many seconds and the per-flight-loop
cadence would waste mapFd-mutex time.
- Logs PRUNE_FF for visibility on each invalidation.
GitHub Actions exposes repository secrets via env-var assignments
unconditionally (`.github/workflows/build.yml:14-17`). When a fork PR
doesn't have the FSC secrets configured, GA passes the env vars as the
empty string — NOT as undefined. The previous `if(DEFINED ENV{...})`
checks treat empty-string-defined as "set", so the cmake fall-through
to the default `"3"` / `"INOP"` stub values never fires. The cmake
substitution at line ~320 then emits bare `-DFSC_PROD_CLIENT_ID=` (no
value); the preprocessor expands `FSC_PROD_CLIENT_ID` to nothing in
`Src/LTFSCharter.cpp`; the Linux CI build fails with "expected
primary-expression before ','" at line 48.
Every fork PR hits this — trivially reproducible with no FSC secrets
in the fork settings, succeeds in the upstream's own CI because the
secrets ARE configured there, succeeds locally because the env vars
aren't set at all (DEFINED is false → default fallback fires).
Fix: add `AND NOT "$ENV{...}" STREQUAL ""` to each of the four FSC
DEFINED-checks. Empty env vars now fall through to the defaults in
the same code path as the not-defined case. Verified locally that
both `unset` and `=""` simulate-as-CI cases produce the expected
"FSCharter Production = Client ID: 3, Client Secret: INOP" output.
TwinFan
added a commit
to TwinFan/XPMP2
that referenced
this pull request
May 18, 2026
Contributed by nebukadnezar, see TwinFan/LiveTraffic#312
TwinFan
added a commit
that referenced
this pull request
May 21, 2026
PR312 - Plane Movement Enhancements
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
LiveTraffic — proposed fixes from nebukadnezar/LiveTraffic fork
Branch:
master, 36 commits ahead ofTwinFan/LiveTraffic@39c8816(v4.4.0).Fork: https://github.com/nebukadnezar/LiveTraffic
Diffstat: 15 files changed, 3668 insertions(+), 60 deletions(-)
Date: 2026-05-13 (initial), updated 2026-05-17 with sections 5–9
This document describes a series of fixes developed against the v4.4.0 release on macOS (Apple Silicon, XCode 26, X-Plane 12) while diagnosing visible aircraft-rendering issues with the RealTraffic feed at busy airports (LSZH, EGLL, KJFK, KDFW, YSSY). Each section gives the symptom, the root cause as observed in the log/code, the fix, and the commit hash.
The change set falls into nine themes:
relOp.txt, and using the v6 operator field as a fallback. (Sections 2, 8)FF****placeholder-hex duplicate prune; revert of an incorrectposTime -= PosAgeadjustment. (Section 9)All code-side changes are heavily commented inline (rationale + thresholds + how the fix interacts with the surrounding flight model). The relOp.txt change lives in
Lib/XPMP2/Resources/relOp.txtand is the only edit outsideSrc//Include/.1. Ground rendering — the "dance" problem
Symptom (user-facing):
Aircraft sitting at gates visibly oscillate / rotate / pivot in place ("dance"). Slow-taxiing aircraft turn jerkily. Landing rollouts slide off the runway centerline toward the next taxiway turn-off. Take-off rolls show the aircraft pointing 45° off motion direction "until well after airborne". Touch-down snaps the nose flat in one frame.
Why this was happening:
LTFlightData::CalcHeadingderives heading fromatan2of consecutive lat/lon deltas (Src/LTFlightData.cpp:1473). Public ADS-B / MLAT feeds carry ~1 Hz positions with several metres of jitter. For a parked aircraft, that noise is the apparent motion vector and the derived heading swings wildly.LTAircraft::CalcAcPos) was usingto.heading()(the next slot's reported heading) as the end-tangent of a Bezier curve between slots, even at runway-roll speeds where the aircraft physically tracks straight along the centerline. The Bezier then arced across the runway corner.pitch.moveTo(0)on touch-down was being clobbered the next frame by a ground-pitch override, snapping the nose flat instead of walking it down smoothly during roll-out.Fixes (in order applied):
98f7f71Constants.htunables for ground stability (hysteresis, rate limit, stationary threshold, holding timeout, ground attitude, pushback band, Bezier ground-min, track-heading threshold). Every constant has an inline rationale comment.511bc67GND_PITCH_DEG/GND_ROLL_DEG(initially 2° / 0°, later 0° / 0°) on the ground each frame inCalcAcPos, with phase exceptions forFPH_ROTATE/FPH_FLARE/FPH_TOUCH_DOWNso the dynamic-pitch transitions aren't clobbered.cbd27faCalcHeading. When both adjacent positions are on-ground with derived gs ≤GND_STATIONARY_GS_KT, reuse the predecessor's filtered heading instead of recomputing from jitter.51d319bGND_HEADING_MAX_RATE_DPS) inCalcAcPos. Even if the target jumps, the rendered nose walks smoothly. Bypassed in the air, where the MovingParam'sdefDurationalready smooths.c517eb5LTFlightData::AddNewPos. AfterGND_HOLDING_TIMEOUT_Sof continuous stationary streak (GND_STATIONARY_GS_KTand below) the channel-side flagbGroundHoldingis set; subsequent feed positions withinGND_HOLDING_TRIVIAL_DIST_Mof the latest stored position are silently dropped — the deque sees a perfectly stable parked aircraft. NewGND_HOLDING_EXIT_CONSECrequires multiple consecutive non-stationary slots to exit holding (a single isolated 2 kt sample from feed jitter won't break the lock).df0f1e3GND_BEZIER_MIN_HEAD_DIFF = 1°vs the 2.5° airborne default) so slow taxi turns get curve-tangent heading rather than the linear MovingParam fallback.22e4d4bCalcHeading. When a ground slot has derived gs between stationary andPUSHBACK_DETECT_GS_MAX_KTAND the track is ≥PUSHBACK_DETECT_HEAD_DIFF_DEGfrom the previous heading, the slot's heading is locked to the predecessor's value. Visually: aircraft moves backward with its nose still pointing at the gate. Per-slot classification — no state machine, no separate code path.a2decf9GND_PITCH_DEGto 0° (commit 3 had 2°; visually wrong on narrow-bodies — turned into a tail-dragger look). Also added unconditionalGND_DIAG_*diagnostic logging inAddNewPos/CalcHeading(tagged so it's grep-able and easy to remove). These logs were essential for the threshold-tuning that follows.a1e0423GND_STATIONARY_GS_KT 0.5→1.5,GND_HEADING_HYSTERESIS_DEG 0.5→4.0,GND_HEADING_MAX_RATE_DPS 60→12(matchesTAXI_TURN_TIME),GND_HOLDING_TRIVIAL_DIST_M 7→15, plus newGND_HOLDING_EXIT_CONSEC = 2. All values now correspond to observed RealTraffic jitter envelopes for parked traffic at gates (gs ≤ 1.1 kt from a few meters of positional noise, ±5–7° heading wobble at slow taxi).51a1cedGND_USE_FEED_HEADING_MAX_KT(10 kt), trust the feed-supplied heading over a track-derived one. RealTraffic delivers a stable heading for parked and pushback aircraft via the transponder data; the previous code was discarding it in favour of jitter-drivenatan2. Fixes the observed pushback misrendering of AA1146 / AA2980 at peak hub time.658dc6bFPH_ROLL_OUTto the ground-pitch override's phase exception list, so theMovingParamwalk fromPITCH_FLAREdown toGND_PITCH_DEG(issued at touchdown) plays out smoothly over ~1 s of roll-out instead of being snapped to 0 the frame after touchdown. Roll is still forced flat in all phases including the dynamic-pitch ones.6aad04cGND_TRACK_HEADING_MIN_KT= 10 kt) skipBezierCurve::Defineand force linear interpolation. The Bezier's end-tangent wasto.heading(), which during runway-end → turn-off transitions arcs the rendered path across the corner. Linear interpolation walks heading towardvec.angle(the actual motion vector) instead. Also skip the half-way retarget toto.heading()under the same condition.9477671vec.speed_kn()(leg-average speed = dist / dt) instead ofGetSpeed_kt()(current rendered speed) for the high-ground-speed check. The original check used the speed coming into the leg, which is wrong for the runway-entry leg where gs enters at ~5 kt and leaves at ~12 kt: the leg-average is 11.8 kt, but the entry speed is 5, so the old check failed and the Bezier was set up across the 89° corner. The next leg's track-heading rule then started walking nose toward the runway, but the MovingParam's natural turn rate is too slow to converge before rotation. Result: aircraft "ADO15 take-off facing 45° away from travel direction until well after airborne". Switching to leg-average fixes this from the moment the aircraft enters the runway.4528131SnapToTaxiwayswhilebGroundHoldingis true.LTAptSnapsynthesises intermediate waypoints between feed samples withheading=NaN, expectingCalcHeadingto fill them in. For a parked aircraft an isolated large feed jump (observed: ACA34 at YSSY, 105 m glitch while the RT app showed the aircraft stationary) gets accepted (exceeds the 15 m trivial-drop threshold), andSnapToTaxiwaysthen synthesises a path of intermediate waypoints between the parked position and the jumped position. The phantom slots' headings come out following the taxi-graph geometry (302° → 256° → 244° in the observed case), not the aircraft's nose direction — and the rendered aircraft walks through them, "dancing" along a non-existent taxi route. Suppressing snap during holding keeps the parked aircraft visually stable; once holding exits (real motion), snap resumes.Files touched:
Include/Constants.h,Include/LTFlightData.h,Src/LTAircraft.cpp,Src/LTFlightData.cpp. Verbose doxygen comments above every modified function explain the why.Diagnostic logging left in: the
GND_DIAG_*tags inLTFlightData::CalcHeadingandLTFlightData::AddNewPosfire unconditionally for every on-ground feed update and heading computation (noGetDebugAcPosflag required). They were critical for the threshold tuning and could be wrapped in a debug flag before merging if you prefer.2. Livery matching —
relOp.txtextension + tail-number detectionSymptom (user-facing):
American Airlines flights showing in random liveries (Air Berlin, Cyprus Airways, etc.). Republic Airways E170s wearing Estonian Air livery. PVL (Pivot Airlines) Dash-8 showing as Austrian. NetJets (EJA) business jets matching Pacific Airways. Many private aircraft (N-prefix registrations) picking arbitrary commercial airline liveries.
Why this was happening:
FDStaticData::airlineCode()returnsopIcaoif set, elsecall.substr(0, 3). The RealTraffic DRCT channel readsRT_DRCT_CallSign(field 13 of the JSON array —"ATC Callsign"per the API v6 docs, ICAO format likeQFA1926) intostat.call, but never setsstat.opIcao. The substring is therefore the only airline source.Three independent root causes were diagnosed from a session's
Log.txtplus the capturedLTRawFD.log:(A) Garbage 3-char codes from tail-number callsigns. Private/GA aircraft commonly transmit their registration as the callsign (
N552FX,9K1876,1I637).call.substr(0, 3)returns"N55","9K1","1I6"— none of which are valid airline ICAO codes. XPMP2 fails to match the airline, falls back to "any livery of this aircraft class", and the tie-breaker is random.(B)
relOp.txtis essentially empty. The shipped file has 13 lines: only Aer Lingus and Ryanair are grouped. None of the major North American codeshare/merger groups (American Eagle, United Express, Delta Connection, AirTran/Southwest, Virgin America/Alaska), no Air Canada family, no KLM/HOP-Air-France/Lufthansa-CityLine grouping. So when a regional carrier (RPA, SKW, EDV, ENY) is requested and no specific livery exists, the matcher has no "related operator" hint and picks randomly.(C) Missing CSL packages. Some (type, airline) combinations are entirely absent from the installed Bluebell library: no
A319_AAL, noA320_AAL, no Republic (*_RPA) at all, no Bombardier Challenger (CL30/CL35/CL60), no Gulfstream G-series widebodies, no A220 (BCS3), no A350 (A35x). These can only be fixed by installing additional repaint packages; we can't conjure CSL models from code.Fix in commit 12 (
1102320): UpdateairlineCode()to detect tail-number-style callsigns. If any of the first three characters ofcallis not an alphabetic letter (ICAO airline codes are always 3 letters), return an empty string rather than the substring. XPMP2 then does type-only matching, which is more faithful to reality than "private jet wearing Austrian livery". Inline doxygen comment explains the rationale.Fix on the data side (not committed to this repo — see note below):
Extended
Lib/XPMP2/Resources/relOp.txtwith ~12 new groups covering North American codeshare and merger relationships, plus the European mainline-group consolidations. Each entry is documented inline (which carriers, which parent, why the grouping was chosen). Notably:Lib/XPMP2/Resources/related.txtwas also updated to merge the Dassault Falcon trijets (F900/FA50/FA7X/FA8X) into the L2J biz-jet group because no CSL library currently ships trijet biz-jet liveries. Without the merge, an FA8X falls outside the related group entirely and lands on a 757 (size-matched fallback to the widest commercial twin). With the merge, FA8X falls back to a Gulfstream / Challenger / Hawker — vastly closer visually.Note on the relOp.txt and related.txt edits: These files live in the XPMP2 submodule (
Lib/XPMP2/Resources/) rather than the LiveTraffic tree. The edits are not part of the 18 commits on this branch — they show asmodified: Lib/XPMP2 (modified content)ingit status. To integrate them cleanly the recommendation would be a separate XPMP2 PR (since XPMP2 is the upstream owner of those files). I have the exact diff ready if you'd like me to send it as a XPMP2 patch in parallel.3. Weather — placeholder QNH responses (commit 17,
82042be)Symptom (user-facing):
At YSSY (real QNH 1034 hPa) landing aircraft were touching down on the terrain about a mile short of the runway threshold.
Why this was happening:
LTRealTraffic::PreProcessWeatheraccepts every successful weather response from the RT endpoint. The endpoint sometimes returns a stripped-down placeholder response for the same query coordinates that previously returned a valid one. Observed sequence at YSSY:The 1013 placeholder overwrites the good 1034. From then on
BaroAltToGeoAlt_ft(d, rtWx.QNH)applies essentially zero correction. At YSSY's real QNH 1034 the missing correction is +560 ft per 1500 ft of baro altitude. Aircraft on final at 1500 ft AGL are rendered at -60 ft relative to the runway elevation; X-Plane's terrain probe sees them already below the surrounding terrain, "lands" them on that terrain, and the user sees aircraft sliding to the ground a mile before the runway.Fix: in
PreProcessWeather, treat a response as a placeholder when all of:ICAOfieldMETARtextwxQNHwithin 0.5 hPa ofHPA_STANDARD(1013.25)…and skip the
SetWeathercall. The last condition lets a genuine 1013-hPa first weather update through; we only reject when a placeholder would overwrite a previously confirmed non-standard value. A debug log lineIgnoring placeholder RealTraffic weather (QNH=…, no ICAO, no METAR); keeping previous QNH=…makes the rejection visible.Verified in a follow-up session: QNH 1034 received once, no subsequent placeholder overwrites observed (RT just happened to consistently return good data this run); guard is in place for the next time the symptom recurs.
4. Channels — re-enable doesn't revive an invalidated channel (commit 18,
21c0a36)Symptom (user-facing):
After RealTraffic experienced a brief network outage (7 connect failures to
rtwa.flyrealtraffic.com:443over 1.2 s), the channel was marked invalid. The user toggled the channel-enable checkbox off and back on. Nothing happened — the channel stayed dead. The user reported "the log shows no errors preventing restart but the channel won't come back".Why this was happening:
LTChannel::shallRun()(the gate the maintenance loop uses to decide whether to callStart()) requires bothIsValid()andIsChannelEnabled(). The channel-enable checkbox is wired toDataRefs::SetChannelEnabled(via the DataRef setter inDataRefs.cpp:1285), which only flipsbChannel[ch]and never touchesbValid. So after an outage:bValid=falsebChannel[ch]=false,bValidunchangedbChannel[ch]=true,bValidstill falseLTFlightDataAcMaintenancepolls every 2 s →shallRun()returns false becauseIsValid()==false→ noStart()callThe intended recovery path was the "Restart Stopped Channels" button in Settings → Basic (which calls
LTFlightDataRestartInvalidChs). Most users will reach for the channel's own checkbox first; that does nothing visible. The single existing caller ofLTChannel::SetEnable(which does callSetValid(true)on enable) is the OpenSky auth-recovery path; it's not used by the UI.Fix: in
DataRefs::SetChannelEnabled, whenbEnable=true, look up theLTChannelviaLTFlightDataGetChand callSetValid(true)on any currently-invalid channel. The off-then-on toggle now behaves as the intuitive UX suggests: a re-enable revives the channel for the next maintenance tick.SetValid(true)also resetserrCnt, so the channel gets a freshCH_MAC_ERR_CNTbudget.Trade-off note: if the underlying failure is permanent (bad credentials, server still down), the channel will re-invalidate on the next attempt — and the user gets immediate visible feedback rather than silent inaction.
What is NOT in this branch (intentionally)
PUSHBACK_DETECT_*heuristic inCalcHeadinghandles the visible "tail-first" symptom adequately; the more sophisticated version would help when the feed's reported heading is stale during the maneuver.A319_CEBexists in the user's CSL. No widebody. The matcher prefers same-type-wrong-airline over wrong-type-same-airline (bit 3 outweighs bit 1 in the quality bitmask), so the A330 falls back to a random A330. CEB has no logical sister carrier to borrow from inrelOp.txt. Documented as a CSL-only fix.5. Ground rendering — feed-vs-track cross-check + snap-order fix
Symptom (user-facing):
Some aircraft taxiing visibly sideways — body pointed one direction, motion in another — particularly noticeable on slow turns and short connecting taxiways. Other aircraft snapping onto the wrong taxi segment because
SnapToTaxiwayswas being given a stale heading.Why this was happening:
Two coupled issues:
LTAptSnapran beforeCalcHeading's feed-vs-track decision, so the snap saw the unfiltered raw heading. Wrong snap candidate selected → aircraft on the wrong taxiway segment.Fixes:
d861869GND_FEED_TRACK_AGREE_DEG = 30°: agree (<30°) → trust feed; opposite (>150°) → trust feed and infer "track reversed (pushback)"; disagree (30°–150°) → fall through to position-derived heading. Catches EHS staleness during slow turns without breaking the trust-feed behaviour at the parked / pushback endpoints.a28ae93SnapToTaxiways. The snap candidate selection now uses the corrected heading, so an aircraft mid-turn won't get snapped onto the segment it's coming FROM.Files touched:
Src/LTFlightData.cpp(cross-check block at line ~1880),Src/LTApt.cpp(snap call ordering).6. Channel reliability — TCP keepalive + no connection reuse + faster wait floor
Symptom (user-facing):
RealTraffic channel hitting
curl error 55: Send failure: Broken piperepeatedly, then going invalid mid-session even with a healthy network. Long gaps between traffic updates causing visible aircraft "freezes" at busy airports.Why this was happening:
RT_DRCT_TRAFFIC_WAIT_FLOOR_Swas set to 5 s, meaning even when traffic was responsive we waited 5 s between requests. At busy airports with rapid feed turnover this lost real-time fidelity.Fixes:
cee1c1cCURLOPT_TCP_KEEPALIVE,CURLOPT_TCP_KEEPIDLE=30,CURLOPT_TCP_KEEPINTVL=15on all curl handles. Sockets get probed before middleboxes idle them out.600904fCURLOPT_FORBID_REUSE=1. Each request gets a fresh connection — minor overhead, eliminates the half-open-socket failure mode.6fb0b1fRT_DRCT_TRAFFIC_WAIT_FLOOR_Sfrom 5 s to 2 s. Combined with the reliability fixes above, the channel is now responsive enough that the lower floor doesn't induce server pressure.Files touched:
Src/LTChannel.cpp,Src/LTRealTraffic.cpp,Include/LTRealTraffic.h.7. Centripetal Catmull-Rom ground spline (commits 24-25,
486b916+78b58db)Symptom (user-facing):
Why this was happening:
The legacy code used per-leg quadratic Bezier curves with
from.heading()andto.heading()as tangents. On 1 Hz ground feeds with several metres of position jitter, the Bezier:t, not arc length, so the rendered position moved at non-constant speed.Fix in commit 24 (
486b916):Replace the per-leg Bezier with a centripetal Catmull-Rom spline through 4 control points:
P0 = posPrev(cached at previous segment switch),P1 = from(current leg start, NOT smoothed — see below),P2 = to(current leg end, smoothed againstP3via a 3-tap binomial kernel),P3 = posNext(cached at this segment's switch, doesn't read live posDeque). The spline:P1at u=0 so segment switches stay seamless when the loop assignsposList.front() = ppos.P2only — turns the interpolating Catmull-Rom into an approximating spline that threads through smoothed endpoints rather than reaching every noisy raw sample.The spline's tangent is the rendered heading (eliminating sliding through turns). New constants in
Constants.h:GND_SPLINE_SMOOTH_WEIGHT,GND_SPLINE_MIN_CHORD_M,GND_SPLINE_MAX_KT,GND_SPLINE_ARC_LUT_N.Fix in commit 25 (
78b58db):Polish pass after observation:
f(linear in time across the leg) yields constant-speed motion along the curve.GND_SPLINE_MIN_CHORD_M= 5 m), skip the spline entirely: linear interp + preservefrom.heading()(avoid noise-driven tangent on near-coincident control points).GND_SPLINE_MAX_KT= 40 kt (rollout, takeoff), skip the spline: linear interp + chord-bearing heading (the chord IS the centerline for straight-line runway motion).from.f.bHeadFixed(orto.f.bHeadFixedper the later pushback fix), interpolate the slot headings instead of using the spline tangent — preserves the held nose during pushback.Files touched:
Src/CoordCalc.cpp(Catmull-Rom evaluator + arc-length LUT, +262 LOC),Include/CoordCalc.h(+119 LOC),Src/LTAircraft.cpp(CalcAcPos ground branch rewrite),Include/Constants.h(4 new tunables).7b. Liftoff / climb altitude smoothing
Symptom (user-facing):
Fixes:
f9941c3liftoffStartAlt_mat the momentFPH_LIFT_OFFfires; forLIFTOFF_BLEND_TIME_S = 10 safterwards, render altitude asliftoffStartAlt_m + smootherstep * max(0, posAlt - liftoffStartAlt_m). C²-continuous (smootherstep, not smoothstep) so there's no acceleration discontinuity at the blend boundary. LocksbOnGrnd=falseduring the blend so the floor clamp doesn't reactivate mid-rise.d36ba8cpastAltSamples_archive (mirrors deque slots but doesn't pop) + Gaussian-weighted local linear regression on altitude in(t - targetTs)relative time (avoids floating-point cancellation at the absolute-epoch scale). Replaces the earlier smoothstep / Hermite / PCHIP attempts that each had pathological cases (PCHIP froze alt at terrain after liftoff, etc.). Floor-clamp + bOnGrnd lock during liftoff blend window to prevent the regression dip flipping bOnGrnd true mid-climb.25c9e33ROTATE_PITCH_MAX_DEG = 10°(was unbounded, leading to tailstrikes). AddFPH_LIFT_OFFto the ground-attitude override exception list so the rotate→liftoff phase transition doesn't have its pitch clobbered mid-rotation.Files touched:
Src/LTAircraft.cpp,Include/LTAircraft.h,Include/Constants.h.8. Parked traffic — show / persist / right-direction (commits 29, 31, 33)
Symptom (user-facing):
The RealTraffic parked-traffic snapshot was meant to populate the gates at major airports with stationary aircraft so the sim isn't visually empty. Observed: only a small fraction of parked traffic actually rendered; many parked aircraft appeared briefly then vanished; many that did render were facing the default 0° (north) regardless of stand orientation. Also: when a real aircraft pulled into a stand and the next 5-min parked-feed re-fetch fired, the just-departed parked-aircraft ghost would re-seed at the same coordinates, creating a brief duplicate at the stand.
Fixes:
5336ce3LTRealTraffic::ProcessParkedAcBuffer: (a) skip dedup whenparkPosis empty — without this, every empty-name aircraft (GA, cargo, remote stands) collapsed into one map slot and only one was kept; (b) copy theSPOS_STARTUPflag onto the position so the Synthetic channel's re-feed path can persist the aircraft; (c) feed the apt.dat startup-loc heading (not the default 0°) onto the position so parked aircraft face the right way.908ca73ProcessParkedAcBufferagainstLTAptAvailable()AT PROCESSING TIME, not just at request time. The ~1–2 s network round-trip between issuing the parked request and the response arriving is long enough for the camera to move and kick off an async apt.dat reload (bAptAvailableflips false). Processing the response then would run startup-loc lookups against a half-rebuilt apt map and mis-place every parked aircraft. Drop the response if layout is not currently available; the next cycle retries.1c9715cevictedHexIdsset (lives for the plugin session) inSyntheticConnectionplus a defence-in-depth distance check inProcessParkedAcBuffer— a hex marked evicted is never re-seeded as long as the new aircraft remains close to that stand.Files touched:
Src/LTRealTraffic.cpp,Src/LTSynthetic.cpp,Include/LTSynthetic.h.8b. Livery — v6 operator field
Symptom (user-facing):
American Eagle regionals showing in random liveries because
opIcaowas never set and the callsign substring trick (section 2) only handles a fraction of cases.Fix in commit 32 (
411ac33):Use the RealTraffic v6 API's dedicated
operator_icaofield (RT_DRCT_OpIcaoinLTRealTraffic.h) and write it intostat.opIcao.airlineCode()then returns the proper operator code directly instead of falling back to the callsign-substring heuristic. Fixes the wrong-livery problem for the entire AA family and any operator whose callsign-substring differs from their ICAO operator code (Eagle codeshares, JetBlue, etc.).Files touched:
Src/LTRealTraffic.cpp(~10 lines),Include/LTRealTraffic.h.9. Pushback state machine + safety valve + duplicate-hex prune
Symptom (user-facing):
The state-free pushback heuristic from section 1 commit 8 handled the simple case (tail-first push from a stand) but broke on rotating pushbacks (where the aircraft is being turned 90° as the tug pushes), on aircraft never in the parked snapshot (live-feed-only, e.g. UAL466), and during the entry/exit transitions (aircraft pivoted to face motion direction at gate; aircraft stuck in "pushing back" state while taxiing out at 20+ kt facing the wrong way). Some aircraft never appeared at all (only 1 deque slot — renderer needs ≥2 for interpolation). Some appeared twice (placeholder
FF****hex + real ICAO hex).Why this was happening:
Several distinct failure modes, identified one by one by capturing logs across many sessions at KDFW:
bGroundHolding(which fires anywhere extended-stationary — runway hold-shorts, taxi pauses, etc.), so the pushback rule false-positived on every non-gate stop.bMotionfor the state machine used the globalGND_STATIONARY_GS_KT = 1.5 kt, but real pushbacks roll at 0.4–1.4 kt, so the machine never engaged at slow-push speeds.track + 180°, which is correct for straight pushes but wrong for rotating pushes (the body actually rotates and the feed reports that rotation).FF****hex IDs; a real-ICAO source later picks up the same callsign with the real hex → two LTFlightData entries, two rendered aircraft.Fixes (in order applied):
95a4189(WIP)CalcHeadingwith feed-vs-track nose-source selection. Committed as WIP because of multiple still-broken edge cases — left as a checkpoint before the rewrite.cf8c6c4posTime -= PosAgeadjustment. The earlier change subtracted RT_DRCT_PosAge from the feed timestamp on the theory thatTimeStampwas a fix-issue time andPosAgewas the report-arrival delay. User clarified field 10 (TimeStamp) is already the position-update epoch; PosAge is info-only. Subtracting double-counted the delay and shifted all positions backward in time. Reverted to using rawTimeStamp.7a072faGATE_DETECT_MAX_DIST_M = 30,GATE_HOLD_MIN_ACCEPT_M = 30,PB_MOTION_GS_KT = 0.3,PB_FEED_NOSE_AGREE_DEG = 30°. NewLTFlightDatamembers:pbState,pbHeldNose,pbUseFeedNose,bGateParked. Gate detection (three paths): SPOS_STARTUP slot, ORbGroundHoldingflip + apt.datLTAptFindStartupLocwithin 30 m. Motion suppression (distance-based): whilebGateParked && pbState==PB_NONE, drop any slot < 30 m from latest accepted; on acceptance clearbGroundHolding. State machine: entry onbGateParked && bMotion && motion rearward of held heading; nose source locked at entry (feedif first-motion feedHdg within 30° of parked heading, elsetrack + 180°); refresh on every motion slot; exit when resumed motion is forward of held nose. Renderer: ground-spline branch inLTAircraft::CalcAcPosnow honoursfrom.bHeadFixed OR to.bHeadFixed(wasfromonly); covers entry-leg case wherefromis a stale live-feed parked slot. Diagnostic:GND_DIAG_GATE,GND_DIAG_GATE_HOLD,GND_DIAG_GATE_RELEASE,PUSHBACK_DIAG ENTRY/ACTIVE/PAUSED/FORCE_EXIT. Also includes per-frameyoungestTSupdate on dropped slots so gate-parked aircraft don't outdate.8c9f16bPB_MAX_GS_KT = 10constant; inCalcHeading, ifpbState ∈ {ACTIVE, PAUSED}and slot gs > 10 kt, force exit to PB_NONE regardless of direction. Catches the failure mode where the held-nose source was wrong at entry (TRACK+180 picked because feedHdg disagreed with parkedHdg, but feedHdg was actually right because the aircraft had rotated during GATE_HOLD suppression). With a wrong reference the directional exit never fires; aircraft taxi-out at 20+ kt while renderer still shows backward nose. Real pushbacks never exceed ~5 kt. Also:FF****placeholder-hex duplicate prune. New staticLTFlightData::PrunePlaceholderHexDuplicates()— two-pass scan ofmapFd: pass 1 collects callsigns held by non-FF entries; pass 2 invalidates any FF entry whose callsign matches. Hooked fromLTRegularUpdateswith a 10 s throttle.Files touched:
Include/Constants.h(+8 constants),Include/LTFlightData.h(PB state members +PrunePlaceholderHexDuplicatesdecl),Src/LTFlightData.cpp(CalcHeading PB block ~300 LOC + AddNewPos gate-detection paths + prune impl),Src/LTAircraft.cpp(spline-branch heading rule),Src/LTMain.cpp(prune hook inLTRegularUpdates),Src/LTApt.cpp.Diagnostic logging left in: the
PUSHBACK_DIAG,GND_DIAG_GATE*tags fire unconditionally and have been essential for diagnosing the iterative fixes. Wrap behind a debug flag before merge if preferred.Files changed (excluding XPMP2 submodule)
.gitignoreInclude/Constants.hInclude/CoordCalc.hInclude/LTAircraft.hliftoffStartAlt_m,touchdownTs,pastAltSamples_archive;LookupAltAtTs(); per-aircraft tug-attach (future use)Include/LTFlightData.hairlineCode()tail-number detection; holding-state members; PB state-machine members;PrunePlaceholderHexDuplicates()Include/LTRealTraffic.hInclude/LTSynthetic.hevictedHexIdsAPI; persistent-eviction marker for gate-handoff durabilitySrc/CoordCalc.cppSrc/DataRefs.cppSetChannelEnabledrevives invalid channel on enableSrc/LTAircraft.cppSrc/LTApt.cppLTAptAvailable()gatingSrc/LTChannel.cppSrc/LTFlightData.cppGND_DIAG_*/PUSHBACK_DIAGdiagnostic loggingSrc/LTMain.cppSrc/LTRealTraffic.cppSrc/LTSynthetic.cppevictedHexIds; gate-handoff durabilityTotal: 15 files, +3668 / -60.
Repo
https://github.com/nebukadnezar/LiveTraffic branch
master, 36 commits ahead ofTwinFan/LiveTraffic@39c8816.