using System.Globalization; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using static XFEEDToolBox.Plugin.JsonWire; namespace XFEEDToolBox.Plugin; internal static unsafe partial class Navigation { private struct LocalIdentity { public int key; public uint kind, market; public ulong body; public fixed byte name[513]; } private struct LocalSelection { public LocalIdentity target; public ulong generation, expires, entity, epoch; public nuint life, fsd, inputRoot; } private struct LocalTargetObservation { public LocalIdentity target; public ulong tick, entity, epoch, world; public nuint life, fsd, inputRoot; public bool valid; } private struct LocalFlightObservation { public ulong tick, utc, sequence, entity, world; public LocalIdentity target; public AffinePose ship, destination; public Vector velocity, targetVelocity, angularVelocity; public double distance, speed, throttle, normalSpeedLimit, cruiseSpeedLimit, accelerationLowerBound; public double safeDropDistance, safeDropSpeed, safeDropAngle, cruiseApproachSeconds, cruiseApproachThrottle; public bool safeDisengage, cruiseParametersVerified, valid, velocityValid, selected, angularVelocityValid; public uint dockingState, requestState, pad; public nuint stationLife; public bool inSupercruise, cruiseAssistAvailable, cruiseAssistEnabled, cruiseAssistEngaged, cruiseAssistMatches, cruiseAssistObscured; public bool dockingVerified, dockingGranted, dockingAssistAllowed, dockingAssistEnabled, dockingAssistEngaged; public bool dockingComputerAvailable, nativeDockingActive, nativeLaunchActive, nativeLaunchRequested, dockingThrottleZero, dockingZeroInputSent, speedZeroInputSent; } private struct LocalActionRequest { public ulong generation, selection, expires, sequence, entity, epoch, world; public nuint life, inputRoot; public uint action; } private static readonly object localLock = new(); private static LocalSelection localSelection = new() { target = new() { key = -1 } }; private static LocalTargetObservation localTargetObservation = new() { target = new() { key = -1 } }; private static LocalFlightObservation localObservation = EmptyLocalObservation(); private static string localSampleStage = "notSampled"; private static ulong localGenerationValue, localConsumedValue, localConfirmedValue, localFailedValue; private static ulong localGeneration { get => Volatile.Read(ref localGenerationValue); set => Volatile.Write(ref localGenerationValue, value); } private static ulong localConsumed { get => Volatile.Read(ref localConsumedValue); set => Volatile.Write(ref localConsumedValue, value); } private static ulong localConfirmed { get => Volatile.Read(ref localConfirmedValue); set => Volatile.Write(ref localConfirmedValue, value); } private static ulong localFailed { get => Volatile.Read(ref localFailedValue); set => Volatile.Write(ref localFailedValue, value); } private static LocalActionRequest localAction; private static ulong localActionGenerationValue, localActionConsumedValue, localActionAppliedValue, localActionFailedValue; private static ulong localActionGeneration { get => Volatile.Read(ref localActionGenerationValue); set => Volatile.Write(ref localActionGenerationValue, value); } private static ulong localActionConsumed { get => Volatile.Read(ref localActionConsumedValue); set => Volatile.Write(ref localActionConsumedValue, value); } private static ulong localActionApplied { get => Volatile.Read(ref localActionAppliedValue); set => Volatile.Write(ref localActionAppliedValue, value); } private static ulong localActionFailed { get => Volatile.Read(ref localActionFailedValue); set => Volatile.Write(ref localActionFailedValue, value); } private struct DockingZeroInputReceipt { public ulong entity, tick; public nuint life, binding, inputService; public uint market; } private static DockingZeroInputReceipt dockingZeroInputReceipt; private static LocalFlightObservation EmptyLocalObservation() => new() { target = new() { key = -1 }, pad = 255 }; private static void ExpireLocal() { lock (localLock) { localSelection.expires = 0; localAction.expires = 0; dockingZeroUpdate.command.expires=0; } lock(normalBoostLock)if(normalBoostPulse is {} pulse)pulse.until=0; ExpireDockingFixture(); } private static nuint LocalDocking(in Snapshot frame) { var value = RegisteredComponent(Pointer(frame.life + 0xC8) - 0x30, Read(image + 0x5F306C8)); return value != 0 && Pointer(value) == image + 0x5315D28 ? value : 0; } private static bool YieldForNativeDocking(in Lease control, in Snapshot frame) { if (!control.yieldOnDockingGrant || control.throttle != 0 || control.localMarket == 0 || control.localMarket == uint.MaxValue) return false; var dock = LocalDocking(frame); if (dock == 0 || Read(dock + 0x614) != 2 || Read(dock + 0x4E9) == 255 || Read(dock + 0x4C0) is not (3 or 4)) return false; var station = Pointer(dock + 0x4E0); var computer = LocalDockingComputer(frame); return station != 0 && Read(station + 0xC0) == 8 && Read(station + 0xB0) == control.localMarket && computer != 0 && (byte)Call(image + 0xED6620, computer) != 0; } private static nuint LocalCruiseAssist(in Snapshot frame) { var value = RegisteredComponent(Pointer(frame.life + 0xC8) - 0x30, Read(image + 0x5F53170)); return value != 0 && Pointer(value) == image + 0x5164800 ? value : 0; } private static nuint LocalDockingComputer(in Snapshot frame) { var value = RegisteredComponent(Pointer(frame.life + 0xC8) - 0x30, Read(image + 0x5F5317C)); return value != 0 && Pointer(value) == image + 0x5165248 && Pointer(Pointer(value) + 0x30) == image + 0xED7D10 && Pointer(Pointer(value) + 0x58) == image + 0xED7E10 && Pointer(Pointer(value) + 0x88) == image + 0xEF8A60 ? value : 0; } private static bool ReadLocalDockingAvailability(nuint computer, nuint expectedTable, nuint expectedGetter) { if (computer < 0x10000 || Pointer(computer) != expectedTable || Pointer(expectedTable + 0x58) != expectedGetter) return false; // The original docking update (0xE9A160) resolves its own module, asks // the module power/health predicate (+0x70), and publishes +0x377 on // this interface. Slot +0x58 reads that result. The SCA predicate at // 0xED7E20 belongs to another class and cannot receive this interface. return (byte)Call(expectedGetter, computer) != 0; } private static bool LocalDockingAvailable(nuint computer) => ReadLocalDockingAvailability(computer, image + 0x5165248, image + 0xED7E10); private static nuint LocalCruiseArrival(in Snapshot frame) { var value = RegisteredComponent(Pointer(frame.life + 0xC8) - 0x30, Read(image + 0x5F34640)); return value != 0 && Pointer(value) == image + 0x533DBF0 ? value : 0; } private static nuint LocalStationUniversalPose(nuint life) { var pose = RegisteredComponent(Pointer(life + 0xC8) - 0x30, Read(image + 0x5F05D3C)); if (pose == 0) return 0; var table = Pointer(pose); if (table == image + 0x553DF60 && Pointer(table + 0x28) == image + 0xF84760 && Pointer(table + 0x60) == image + 0x3CBC040) return pose + 0xA8; if (IsPose(pose) && Pointer(table + 0x60) == image + 0x2D9C200) return pose + 0xE8; return 0; } private static nuint LocalRecord(nuint navigation, int key) { var count = Read(navigation + 0x2C0); var capacity = Read(navigation + 0x2C8); var storage = Pointer(navigation + 0x2D0); if (storage == 0 || capacity < 8 || capacity > 16384 || (capacity & (capacity - 1)) != 0 || count > 1024 || count > capacity) return 0; var entries = storage + (nuint)(((capacity >> 3) + 7) & ~7UL); for (ulong i = 0; i < capacity; i++) if (((Read(storage + (nuint)(i >> 3)) >> (int)(i & 7)) & 1) != 0) { var entry = entries + (nuint)i * 16; if (Read(entry) == key) return Pointer(entry + 8); } return 0; } private static bool ReadLocalIdentity(nuint record, int key, out LocalIdentity output) { output = new() { key = -1 }; if (record == 0 || Read(record + 0x188) != 1) return false; LocalIdentity value = new() { key = key, body = Read(record + 0x20), kind = Read(record + 0x184), market = Read(record + 0x130) }; var text = Pointer(record + 0x140); if (text == 0) return false; var length = Read(text); if (length == 0 || length >= 513) return false; var used = 0; for (ulong i = 0; i < length; i++) { var c = Read(text + 0x14 + (nuint)i); if (c == 0) return false; if (c >= 32 && c != 127) value.name[used++] = c; } while (used != 0 && value.name[used - 1] == (byte)' ') value.name[--used] = 0; var leading = 0; while (leading < used && value.name[leading] == (byte)' ') leading++; if (leading != 0) { new Span(value.name + leading, used - leading).CopyTo(new Span(value.name, 513)); used -= leading; value.name[used] = 0; } if (value.body == 0 || value.kind == 0 || used == 0) return false; output = value; return true; } private static bool SameLocal(in LocalIdentity a, in LocalIdentity b) { if (a.key != b.key || a.body != b.body || a.kind != b.kind || a.market != b.market) return false; fixed (byte* left = a.name) fixed (byte* right = b.name) return new ReadOnlySpan(left, 513).SequenceEqual(new ReadOnlySpan(right, 513)); } private static bool LocalRecordMatches(nuint navigation, in LocalIdentity wanted) => ReadLocalIdentity(LocalRecord(navigation, wanted.key), wanted.key, out var actual) && SameLocal(actual, wanted); private static LocalTargetObservation ObserveLocalTarget(in Snapshot frame) { var result = new LocalTargetObservation { target = new() { key = -1 } }; try { if (!frame.valid || GetTickCount64() - frame.tick > 200 || !BoundFsd(frame, frame.fsd)) return result; var nav = Pointer(frame.fsd + 0x338); if (Pointer(Pointer(nav) + 0x20) != image + 0x2EC8100 || Pointer(Pointer(nav) + 0xB8) != image + 0x2EC8D60) return result; var key = Read(nav + 0x38C); if (!ReadLocalIdentity(LocalRecord(nav, key), key, out result.target) || (result.target.body & 0x007FFFFFFFFFFFFFUL) != (frame.referenceWorld & 0x007FFFFFFFFFFFFFUL)) return result; result.tick = frame.tick; result.entity = frame.entity; result.epoch = controllerEpoch; result.world = frame.referenceWorld; result.life = frame.life; result.fsd = frame.fsd; result.inputRoot = frame.inputRoot; result.valid = Read(nav + 0x38C) == key && LocalRecordMatches(nav, result.target) && Read(frame.transform + 0xE8) == frame.referenceWorld; } catch { result.valid = false; } return result; } private static bool LocalSelectionAcknowledged(in LocalSelection wanted, in LocalTargetObservation observed, ulong confirmedGeneration, ulong now, ulong epoch, bool playerOverridden) => !playerOverridden && wanted.generation != 0 && confirmedGeneration == wanted.generation && observed.valid && observed.tick <= now && now - observed.tick <= 200 && observed.epoch == epoch && wanted.epoch == epoch && observed.entity == wanted.entity && observed.life == wanted.life && observed.fsd == wanted.fsd && observed.inputRoot == wanted.inputRoot && (observed.world & 0x007FFFFFFFFFFFFFUL) == (wanted.target.body & 0x007FFFFFFFFFFFFFUL) && SameLocal(observed.target, wanted.target); private static bool LocalControlMatches(in Lease control, in Snapshot frame) { try { var navigation = Pointer(frame.fsd + 0x338); var record = LocalRecord(navigation, control.localKey); return control.local && control.world == frame.referenceWorld && Read(navigation + 0x38C) == control.localKey && record != 0 && Read(record + 0x188) == 1 && Read(record + 0x20) == control.localBody && Read(record + 0x184) == control.localKind && Read(record + 0x130) == control.localMarket; } catch { return false; } } private static bool SelectLocalTargetImpl(in Snapshot frame, in LocalSelection wanted) { try { var now = GetTickCount64(); if (!Enabled() || !inGame || !frame.valid || !frame.healthValid || frame.healthEmergency || now - frame.tick > 200 || wanted.expires <= now || wanted.epoch != controllerEpoch || overridden || wanted.entity != frame.entity || wanted.life != frame.life || wanted.fsd != frame.fsd || wanted.inputRoot != frame.inputRoot || !BoundFsd(frame, frame.fsd) || !LocalSelectionFsdReady(frame.rawFsdState,fsdOwned)) return false; var nav = Pointer(frame.fsd + 0x338); if (Pointer(Pointer(nav) + 0x20) != image + 0x2EC8100 || Pointer(Pointer(nav) + 0x90) != image + 0x2ED6840 || Pointer(Pointer(nav) + 0xB8) != image + 0x2EC8D60 || (wanted.target.body & 0x007FFFFFFFFFFFFFUL) != (frame.referenceWorld & 0x007FFFFFFFFFFFFFUL) || !LocalRecordMatches(nav, wanted.target)) return false; if (Read(nav + 0x38C) != wanted.target.key) Call(image + 0x2ED6840, nav, unchecked((ulong)wanted.target.key), 1, 0); return Read(nav + 0x38C) == wanted.target.key && LocalRecordMatches(nav, wanted.target); } catch { return false; } } private static void SelectLocalTarget(in Snapshot frame) { LocalSelection wanted; lock (localLock) wanted = localSelection; if (wanted.generation == 0 || localConsumed == wanted.generation) return; if (SelectLocalTargetImpl(frame, wanted)) localConfirmed = wanted.generation; else localFailed = wanted.generation; localConsumed = wanted.generation; } private static double LocalDecodeFloat(nuint at) { var bits = Read(at); return BitConverter.UInt32BitsToSingle((uint)bits ^ (uint)(bits >> 32)); } private static bool LocalBodyPose(nuint body, nuint universal, nuint resource, out AffinePose output) { AffinePose value = default; var valid = BodyPose(body, universal, resource, &value); output = value; return valid; } private static bool LocalEncodedFlag(nuint at) { var bits = Read(at); return (bits & 255) != (bits >> 8); } private static bool SampleLocalFlight(in Snapshot frame, out LocalFlightObservation output, out string stage) { output = EmptyLocalObservation(); var next = EmptyLocalObservation(); stage = "snapshot"; try { if (!frame.valid || GetTickCount64() - frame.tick > 200 || !BoundFsd(frame, frame.fsd)) return false; stage = "navigationInterface"; var nav = Pointer(frame.fsd + 0x338); if (Pointer(Pointer(nav) + 0x20) != image + 0x2EC8100 || Pointer(Pointer(nav) + 0xB8) != image + 0x2EC8D60) return false; stage = "selectedIdentity"; var key = Read(nav + 0x38C); var record = LocalRecord(nav, key); if (!ReadLocalIdentity(record, key, out next.target) || (next.target.body & 0x007FFFFFFFFFFFFFUL) != (frame.referenceWorld & 0x007FFFFFFFFFFFFFUL)) return false; stage = "loadedSystem"; var service = Pointer(frame.fsd + 0x6A0); var resource = LoadedSystem(service, frame.referenceWorld); if (resource == 0 || Pointer(Pointer(service) + 0x60) != image + BodyLookup) return false; stage = "bodyGeometry"; var bodies = Pointer(resource + 0x140); var capacity = Read(resource + 0x148); var shipBody = BodyMapEntry(bodies, capacity, frame.referenceWorld, 16, false); var targetBody = BodyMapEntry(bodies, capacity, next.target.body, 16, false); if (shipBody == 0 || targetBody == 0 || !LocalBodyPose(shipBody, frame.transform + 0xE8, resource, out next.ship) || !LocalBodyPose(targetBody, record + 0x20, resource, out next.destination)) return false; var delta = Subtract(next.destination.position, next.ship.position); next.distance = Math.Sqrt(Dot(delta, delta)); next.speed = frame.speed; next.throttle = Read(frame.inputRoot + 0x70); next.angularVelocity = frame.angularVelocityLocal; next.angularVelocityValid = frame.angularVelocityValid; stage = "distanceAndThrottle"; if (!double.IsFinite(next.distance) || next.distance < 0 || !double.IsFinite(next.throttle) || Math.Abs(next.throttle) > 1.001) return false; stage = "dockingState"; var computer = LocalDockingComputer(frame); if (computer != 0) { next.dockingComputerAvailable = LocalDockingAvailable(computer); next.nativeDockingActive = (byte)Call(image + 0xED6620, computer) != 0; next.nativeLaunchActive = (byte)Call(image + 0xED7D10, computer) != 0; next.nativeLaunchRequested = Read(computer + 0x374) != 0; var controller = Pointer(frame.inputRoot + 0x2F8); next.dockingThrottleZero = controller != 0 && Pointer(computer + 0x240) == controller && Pointer(Pointer(controller) + 0x158) == image + ThrottleSetter &&Read(controller + 0x680)==0&&ReadOriginalDockingThrottle(computer,out var sourceThrottle)&&Math.Abs(sourceThrottle)<=.001f; next.speedZeroInputSent = dockingZeroInputReceipt.entity == frame.entity && dockingZeroInputReceipt.life == frame.life && dockingZeroInputReceipt.inputService == Pointer(computer + 0x230) && frame.tick >= dockingZeroInputReceipt.tick && frame.tick - dockingZeroInputReceipt.tick < 30000; next.dockingZeroInputSent = next.speedZeroInputSent && dockingZeroInputReceipt.market == next.target.market; } var dock = LocalDocking(frame); if (dock != 0) { next.dockingState = Read(dock + 0x4C0); next.requestState = Read(dock + 0x614); next.pad = Read(dock + 0x4E9); var station = Pointer(dock + 0x4E0); if (station != 0 && Read(station + 0xC0) == 8) next.stationLife = station; next.dockingAssistEnabled = Read(dock + 0x5BB) != 0; if (next.stationLife != 0 && Read(next.stationLife + 0xB0) == next.target.market) { next.dockingVerified = true; next.dockingGranted = next.dockingState is 3 or 4 && next.requestState == 2 && next.pad != 255; var stationService = RegisteredComponent(Pointer(next.stationLife + 0xC8) - 0x30, Read(image + 0x5F771D0)); if (stationService != 0) { var table = Pointer(stationService); var activeFunction = Pointer(table + 0x28); var allowed = Pointer(table + 0x58); if (InImage(activeFunction) && InImage(allowed)) { next.dockingAssistAllowed = (byte)Call(allowed, stationService) != 0; next.dockingAssistEngaged = (byte)Call(activeFunction, stationService) != 0; } } } } stage = "motionLimits"; next.inSupercruise = LocalCruiseArrival(frame) != 0; next.normalSpeedLimit = (double)Read(frame.physics + 0x750) * Read(frame.physics + 0x910); var x = (double)Read(frame.physics + 0x600); var y = (double)Read(frame.physics + 0x604); var z = (double)Read(frame.physics + 0x608); next.accelerationLowerBound = Math.Min(x, Math.Min(y, z)) * .25; next.cruiseSpeedLimit = next.inSupercruise && frame.speedFraction > .001 ? frame.speed / frame.speedFraction : next.normalSpeedLimit; var arrival = LocalCruiseArrival(frame); if (arrival != 0) { stage = "cruiseInterface"; var table = Pointer(arrival); if (Pointer(table + 0x38) != image + 0x2EA4EE0 || Pointer(table + 0x118) != image + 0x2EA4EC0 || Pointer(table + 0x68) != image + 0x2EAD330) return false; next.cruiseSpeedLimit = CallFloat(image + 0x2EA4EE0, arrival); var limits = (nuint)Call(image + 0x2EA4EC0, arrival); if (limits != arrival + 0x5A8) return false; next.safeDropDistance = LocalDecodeFloat(limits + 0x28); next.safeDropSpeed = LocalDecodeFloat(limits + 0x48); next.safeDropAngle = LocalDecodeFloat(limits + 0x68); var alignment = next.distance > 0 ? Math.Acos(Math.Clamp(Dot(next.ship.forward, Scale(delta, 1 / next.distance)), -1, 1)) : Math.PI; next.safeDisengage = LocalEncodedFlag(limits + 0xE8) && LocalEncodedFlag(limits + 0x108) && LocalEncodedFlag(limits + 0x128) && double.IsFinite(next.safeDropDistance) && next.safeDropDistance > 0 && next.distance < next.safeDropDistance && double.IsFinite(next.safeDropSpeed) && next.safeDropSpeed > 0 && next.speed < next.safeDropSpeed && double.IsFinite(next.safeDropAngle) && next.safeDropAngle > 0 && next.safeDropAngle < 1.571 && alignment < next.safeDropAngle; var helper = RegisteredComponent(Pointer(frame.life + 0xC8) - 0x30, Read(image + 0x5F53174)); var assist = LocalCruiseAssist(frame); if (helper != 0 && Pointer(helper) == image + 0x5164C78 && assist != 0) { next.cruiseApproachSeconds = Read(Pointer(helper - 8) + 0x50); next.cruiseApproachThrottle = Read(Pointer(assist - 8) + 0x10); next.cruiseParametersVerified = double.IsFinite(next.cruiseApproachSeconds) && next.cruiseApproachSeconds > 0 && next.cruiseApproachSeconds <= 120 && double.IsFinite(next.cruiseApproachThrottle) && next.cruiseApproachThrottle > 0 && next.cruiseApproachThrottle <= 1; } } stage = "motionLimits"; if (!double.IsFinite(next.normalSpeedLimit) || next.normalSpeedLimit <= 0 || !double.IsFinite(next.cruiseSpeedLimit) || next.cruiseSpeedLimit <= 0 || !double.IsFinite(next.accelerationLowerBound) || next.accelerationLowerBound <= 0) return false; stage = "cruiseAssist"; var cruiseAssist = LocalCruiseAssist(frame); if (cruiseAssist != 0) { next.cruiseAssistAvailable = (byte)Call(image + 0xECAF00, cruiseAssist) != 0; next.cruiseAssistEnabled = Read(cruiseAssist + 0x256) != 0; next.cruiseAssistEngaged = Read(cruiseAssist + 0x257) != 0; next.cruiseAssistMatches = (byte)Call(image + 0xED8760, cruiseAssist, record) != 0; next.cruiseAssistObscured = (byte)Call(image + 0xED8A50, cruiseAssist, record) != 0; } next.tick = frame.tick; next.utc = frame.utcMilliseconds; next.sequence = frame.sequence; next.entity = frame.entity; next.world = frame.referenceWorld; next.selected = true; next.valid = Read(nav + 0x38C) == key && LocalRecordMatches(nav, next.target) && Read(frame.transform + 0xE8) == frame.referenceWorld; stage = next.valid ? "ready" : "identityChanged"; return next.valid; } catch (Exception error) { stage += ":" + error.GetType().Name; return false; } finally { output = next; } } private static void PollLocalFlight(in Snapshot frame) { LocalFlightObservation before; lock (localLock) before = localObservation; if (before.sequence == frame.sequence && before.tick != 0 && GetTickCount64() - before.tick < 100) return; var selected = ObserveLocalTarget(frame); var next = EmptyLocalObservation(); if (SampleLocalFlight(frame, out next, out var stage)) { var seconds = ((double)next.tick - before.tick) * .001; if (before.valid && before.entity == next.entity && before.world == next.world && SameLocal(before.target, next.target) && seconds >= .005 && seconds <= .3) { next.velocity = Scale(Subtract(next.ship.position, before.ship.position), 1 / seconds); next.targetVelocity = Scale(Subtract(next.destination.position, before.destination.position), 1 / seconds); next.velocityValid = Finite(next.velocity) && Finite(next.targetVelocity); } } lock (localLock) { localObservation = next; localTargetObservation = selected; localSampleStage = stage; } } private static nuint LocalEntityFromDirectory(nuint context, nuint playerLife, ulong playerEntity, ulong wanted) { try { var count = Read(context + 0x148); var capacity = Read(context + 0x150); var storage = Pointer(context + 0x158); if (storage == 0 || capacity < 8 || capacity > 65536 || (capacity & (capacity - 1)) != 0 || count > capacity) return 0; var entries = storage + (nuint)(((capacity >> 3) + 7) & ~7UL); var own = false; nuint found = 0; for (ulong i = 0; i < capacity; i++) if (((Read(storage + (nuint)(i >> 3)) >> (int)(i & 7)) & 1) != 0) { var at = entries + (nuint)i * 16; var key = Read(at); var life = Pointer(at + 8); if (key == playerEntity) own = life == playerLife; if (key == wanted) found = life; } if (!own || found == 0 || Read(context + 0x148) != count || Read(context + 0x150) != capacity || Pointer(context + 0x158) != storage || Read(found + 0xC0) != 8 || Read(found + 0xB0) != wanted) return 0; return found; } catch { return 0; } } private static bool LocalDockingTargetKind(uint kind)=>XFEEDToolBox.Core.Navigation.NativeDockingTargetKinds.SupportsDocking(kind); private static bool RequestLocalDocking(in Snapshot frame, in LocalFlightObservation observed) { var nav = Pointer(frame.fsd + 0x338); var dock = LocalDocking(frame); var record = LocalRecord(nav, observed.target.key); if (dock == 0 || record == 0 || observed.inSupercruise || observed.distance > 7500 || observed.target.market == 0 || observed.target.market == uint.MaxValue || !LocalDockingTargetKind(observed.target.kind) || Pointer(Pointer(dock) + 0xF8) != image + 0x2C65C30) return false; var context = Pointer(frame.life + 0x38); var token = (ulong)Read(record + 0x130); if (context == 0 || token == 0 || token == uint.MaxValue) return false; var existing = LocalEntityFromDirectory(context, frame.life, frame.entity, token); if (existing == 0) return false; nuint station = 0; Call(image + 0x62D0A0, (ulong)(nuint)(&station), (ulong)(nuint)(&existing)); try { if (station == 0 || Read(station + 0xC0) != 8 || Read(station + 0xB0) != token) return false; var stationPose = LocalStationUniversalPose(station); var resource = LoadedSystem(Pointer(frame.fsd + 0x6A0), frame.referenceWorld); if (stationPose == 0 || resource == 0) return false; var stationWorld = Read(stationPose); if ((stationWorld & 0x007FFFFFFFFFFFFFUL) != (frame.referenceWorld & 0x007FFFFFFFFFFFFFUL)) return false; var body = BodyMapEntry(Pointer(resource + 0x140), Read(resource + 0x148), stationWorld, 16, false); if (body == 0 || !LocalBodyPose(body, stationPose, resource, out var pose)) return false; var delta = Subtract(pose.position, observed.ship.position); if (Dot(delta, delta) > 7500.0 * 7500.0 || !LocalRecordMatches(nav, observed.target) || Read(nav + 0x38C) != observed.target.key) return false; EndNormalBoostPulse(); if(observed.dockingComputerAvailable) { var controller=Pointer(frame.inputRoot+0x2F8); if(controller==0||Pointer(Pointer(controller)+0x158)!=image+ThrottleSetter||!SendOriginalDockingZeroInput(frame,controller,observed.target.market))return false; } return (byte)Call(image + 0x2C65C30, dock, (ulong)(nuint)(&station)) != 0; } finally { Call(image + 0x62F3A0, (ulong)(nuint)(&station)); } } private static bool SendOriginalDockingZeroInput(in Snapshot frame, nuint controller, uint market) { try { var computer = LocalDockingComputer(frame); var actions = computer != 0 ? Pointer(computer + 0x230) : 0; if (computer == 0 || Pointer(computer + 0x240) != controller || actions == 0 || Pointer(actions) != image + 0x512FD90 || Pointer(Pointer(actions) + 0x40) != image + 0x8FEDC0) return false; var scratch = stackalloc byte[47]; var temporary = AlignServiceScratch(scratch); new Span(temporary, 32).Clear(); try { Call(image + 0x8FEDC0, actions, (ulong)(nuint)temporary, image + 0x531A568); var input = Pointer((nuint)temporary); if (input == 0 || Read(input) <= 0 || Read(input + 0x1C) == uint.MaxValue) return false; Write(input + 0x24, (byte)(Read(input + 0x24) | 1)); Write(input + 0x1C, unchecked(Read(input + 0x1C) + 1)); Call(image + ThrottleSetter, controller, F32(0)); dockingZeroInputReceipt = new() { entity = frame.entity, tick = frame.tick, life = frame.life, binding = input, inputService = actions, market = market }; return true; } finally { Call(image + 0x204010, (ulong)(nuint)temporary); } } catch { return false; } } private static bool LocalDockingZeroConfirmed(bool nativeDuty,bool inputSent,float controllerThrottle,double actualThrottle) =>nativeDuty||inputSent&&controllerThrottle==0&&double.IsFinite(actualThrottle)&&Math.Abs(actualThrottle)<=.001; private static bool ApplyLocalAction(in Snapshot frame, in LocalActionRequest command, in LocalSelection selection, in LocalFlightObservation observed) { try { var now = GetTickCount64(); if (GetCurrentThreadId() != owningThread || !Enabled() || !inGame || !frame.valid || !frame.healthValid || frame.hullFraction<=0 || frame.healthEmergency || overridden || command.expires <= now || command.entity != frame.entity || command.life != frame.life || command.inputRoot != frame.inputRoot || command.world != frame.referenceWorld || command.epoch != controllerEpoch || command.selection != selection.generation || localConfirmed != selection.generation || !observed.valid || !SameLocal(observed.target, selection.target) || now - observed.tick > 200 || now - frame.tick > 200 || !BoundFsd(frame, frame.fsd) || !LocalRecordMatches(Pointer(frame.fsd + 0x338), selection.target)) return false; if (command.action == 1) return RequestLocalDocking(frame, observed); if(command.action==13)return RequestNormalBoost(frame); if (command.action == 12) { if (!observed.nativeDockingActive) return true; if (observed.nativeLaunchActive || observed.inSupercruise) return false; var computer = LocalDockingComputer(frame); var activity = RegisteredComponent(Pointer(frame.life + 0xC8) - 0x30, Read(image + 0x5F44C98)); if (computer == 0 || activity == 0 || Pointer(activity) != image + 0x5165E08 || Pointer(Pointer(activity) + 0x30) != image + 0xEACAD0 || (byte)Call(image + 0xED6620, computer) == 0) return false; Call(image + 0xEACAD0, activity); var controller = Pointer(frame.inputRoot + 0x2F8); if (controller == 0 || Pointer(computer + 0x240) != controller || Pointer(Pointer(controller) + 0x158) != image + ThrottleSetter) return false; Call(image + ThrottleSetter, controller, F32(0)); return true; } if (command.action == 9) { var computer = LocalDockingComputer(frame); var dock = LocalDocking(frame); var activity = RegisteredComponent(Pointer(frame.life + 0xC8) - 0x30, Read(image + 0x5F44C98)); if (computer == 0 || dock == 0 || activity == 0 || !observed.nativeLaunchActive && !observed.nativeLaunchRequested || !observed.dockingVerified || observed.inSupercruise || Pointer(activity) != image + 0x5165E08 || Pointer(Pointer(activity) + 0x30) != image + 0xEACAD0 || Pointer(Pointer(computer) + 0x80) != image + 0xEE8710) return false; if (observed.nativeLaunchActive) Call(image + 0xEACAD0, activity); if (Read(computer + 0x374) != 0) { nuint empty = 0; Call(image + 0xEE8710, computer, ulong.MaxValue, (ulong)(nuint)(&empty)); } var controller = Pointer(frame.inputRoot + 0x2F8); if (controller == 0 || Pointer(computer + 0x240) != controller || Pointer(Pointer(controller) + 0x158) != image + ThrottleSetter) return false; Call(image + ThrottleSetter, controller, F32(0)); return true; } if (command.action == 8) { var dock = LocalDocking(frame); var state = dock != 0 ? Read(dock + 0x4C0) : 0; var computer = LocalDockingComputer(frame); if (dock == 0 || computer == 0 || !observed.dockingComputerAvailable || !LocalDockingAvailable(computer) || observed.inSupercruise || state is not (5 or 7) || observed.distance > 7500 || Pointer(Pointer(dock) + 0xF0) != image + 0x2C65CD0) return false; var station = Pointer(dock + 0x4E0); if (station == 0 || Read(station + 0xC0) != 8 || Read(station + 0xB0) != observed.target.market) return false; Call(image + 0xEF8A60, computer); return (byte)Call(image + 0x2C65CD0, dock, (ulong)(nuint)(&station)) != 0; } if (command.action == 7) { var arrival = LocalCruiseArrival(frame); if (arrival == 0 || !observed.safeDisengage || !observed.inSupercruise || frame.rawFsdState != 2 || fsdOwned || observed.target.kind <= 3 || observed.target.kind == 39) return false; Call(image + 0x2EAD330, arrival); return true; } if (command.action == 6) { if (!observed.inSupercruise || frame.rawFsdState != 2 || fsdOwned || frame.speed > 100000 || observed.cruiseAssistEngaged) return false; Lease control; lock (leaseLock) control = lease; if (control.expires <= now || !LocalControlMatches(control, frame) || control.throttle != 0) return false; var next = new FsdLease { expires = command.expires, sequence = command.sequence, generation = NextFsdGeneration(), entity = frame.entity, fsd = frame.fsd, inputRoot = frame.inputRoot, life = frame.life, command = 4, heatCeiling = 80, local = true }; lock (leaseLock) fsdLease = next; return true; } if (command.action == 5) { if (observed.inSupercruise || frame.rawFsdState != 2 || fsdOwned || HeatHudPercent(frame) >= 90 || Read(frame.inputRoot + 0x60 + 0xA9) != 0 || Read(frame.inputRoot + 0x60 + 0xB1) != 0 || Read(frame.inputRoot + 0x60 + 0xBB) != 0) return false; Lease control; lock (leaseLock) control = lease; if (control.expires <= now || !LocalControlMatches(control, frame) || control.throttle < .75f) return false; var next = new FsdLease { expires = command.expires, sequence = command.sequence, generation = NextFsdGeneration(), entity = frame.entity, fsd = frame.fsd, inputRoot = frame.inputRoot, life = frame.life, command = 1, heatCeiling = 90, supercruiseEntry = true, local = true }; lock (leaseLock) fsdLease = next; return true; } if (command.action == 10) { var dock = LocalDocking(frame); var computer = LocalDockingComputer(frame); var controller = Pointer(frame.inputRoot + 0x2F8); if (dock == 0 || computer == 0 || !observed.dockingComputerAvailable || !LocalDockingAvailable(computer) || controller == 0 || Pointer(computer + 0x240) != controller || !observed.dockingVerified || !observed.dockingGranted || observed.inSupercruise || observed.distance > 7500 || Read(dock + 0x614) != 2 || Read(dock + 0x4C0) is not (3 or 4) || Read(dock + 0x4E9) == 255 || Pointer(Pointer(controller) + 0x158) != image + ThrottleSetter) return false; var station = Pointer(dock + 0x4E0); if (station == 0 || Read(station + 0xC0) != 8 || Read(station + 0xB0) != observed.target.market) return false; var actualThrottle=Read(frame.inputRoot+0x70); if((byte)Call(image+0xED6620,computer)!=0)return true; bool released;lock(leaseLock)released=lease.expires<=now&&fsdLease.expires<=now; if(!released||fsdOwned)return false; var sourceVerified=ReadOriginalDockingThrottle(computer,out var sourceThrottle); if(!LocalDockingSourceZeroConfirmed(false,observed.dockingZeroInputSent,Read(controller+0x680),actualThrottle,sourceVerified?sourceThrottle:double.NaN) &&!SendOriginalDockingZeroInput(frame,controller,observed.target.market))return false; if(!CommitOriginalDockingZeroAxis(frame.inputRoot,controller,actualThrottle,true,released,out _))return false; lock(localLock)dockingZeroUpdate=new(){command=command,computer=computer,controller=controller,fsd=frame.fsd,market=observed.target.market,throttle=actualThrottle}; return true; } if (command.action == 11) { var controller = Pointer(frame.inputRoot + 0x2F8); if (observed.inSupercruise || frame.rawFsdState != 2 || fsdOwned || controller == 0 || Pointer(Pointer(controller) + 0x158) != image + ThrottleSetter) return false; return SendOriginalDockingZeroInput(frame, controller, 0); } var assist = LocalCruiseAssist(frame); if (command.action == 2) { if (assist == 0 || !observed.inSupercruise || !observed.cruiseAssistAvailable || (byte)Call(image + 0xECAF00, assist) == 0 || observed.cruiseAssistObscured || observed.distance < 1) return false; if (observed.cruiseAssistEnabled && observed.cruiseAssistMatches) return true; var direction = Scale(Subtract(observed.destination.position, observed.ship.position), 1 / observed.distance); if (Dot(direction, observed.ship.forward) < Math.Cos(5.0 * Math.PI / 180)) return false; var controller = Pointer(frame.inputRoot + 0x2F8); if (controller == 0 || Pointer(Pointer(controller) + 0x158) != image + ThrottleSetter || !observed.cruiseParametersVerified) return false; Call(image + ThrottleSetter, controller, F32((float)observed.cruiseApproachThrottle)); Call(image + 0xEEA430, assist, 1); return true; } if (command.action == 3) { if (assist == 0 || !observed.cruiseAssistMatches || !observed.cruiseAssistEnabled && !observed.cruiseAssistEngaged) return true; Call(image + 0xEEA430, assist, 0); return true; } return false; } catch { return false; } } private static void PollLocalAction(in Snapshot frame) { PollNormalBoostPulse(frame); LocalActionRequest command; LocalSelection selection; LocalFlightObservation observed; lock (localLock) { command = localAction; selection = localSelection; observed = localObservation; } if (command.generation == 0 || command.generation == localActionConsumed) return; if (ApplyLocalAction(frame, command, selection, observed)) localActionApplied = command.generation; else localActionFailed = command.generation; localActionConsumed = command.generation; } private static string LocalIdentityName(in LocalIdentity value) { fixed (byte* text = value.name) { var length = 0; while (length < 513 && text[length] != 0) length++; return Encoding.UTF8.GetString(new ReadOnlySpan(text, length)); } } private static JsonObject LocalIdentityJson(in LocalIdentity value) => Obj(("key", value.key), ("bodyAddress", value.body.ToString(CultureInfo.InvariantCulture)), ("kind", value.kind), ("marketId", value.market), ("name", LocalIdentityName(value))); private static JsonArray LocalVector(Vector value) => Arr(value.x, value.y, value.z); private static JsonObject LocalFlightRead(in Snapshot frame) { LocalFlightObservation value;DockingMapperReceipt mapper; lock(localLock){value=localObservation;mapper=dockingInputMapperReceipt;}var now=GetTickCount64(); if (!frame.valid || !value.valid || value.entity != frame.entity || value.world != frame.referenceWorld || now - value.tick > 200) throw new InvalidOperationException("Local target telemetry unavailable or stale"); return Obj(("sequence", value.sequence), ("observedAtUnixMilliseconds", value.utc), ("entityId", value.entity.ToString(CultureInfo.InvariantCulture)), ("referenceWorldAddress", value.world.ToString(CultureInfo.InvariantCulture)), ("geometryVerified", true), ("selected", value.selected), ("target", LocalIdentityJson(value.target)), ("position", LocalVector(value.ship.position)), ("forward", LocalVector(value.ship.forward)), ("up", LocalVector(value.ship.up)), ("targetPosition", LocalVector(value.destination.position)), ("velocity", LocalVector(value.velocity)), ("targetVelocity", LocalVector(value.targetVelocity)), ("velocityVerified", value.velocityValid), ("angularVelocity", LocalVector(value.angularVelocity)), ("angularVelocityVerified", value.angularVelocityValid), ("speedZeroInputSent", value.speedZeroInputSent), ("distanceMetres", value.distance), ("speedMetresPerSecond", value.speed), ("throttle", value.throttle), ("rawDockingState", value.dockingState), ("rawRequestState", value.requestState), ("rawPad", value.pad), ("normalSpeedLimit", value.normalSpeedLimit), ("cruiseSpeedLimit", value.cruiseSpeedLimit), ("accelerationLowerBound", value.accelerationLowerBound), ("safeDisengage", value.safeDisengage), ("safeDropDistance", value.safeDropDistance), ("safeDropSpeed", value.safeDropSpeed), ("cruiseParametersVerified", value.cruiseParametersVerified), ("cruiseApproachSeconds", value.cruiseApproachSeconds), ("cruiseApproachThrottle", value.cruiseApproachThrottle), ("rawFsdState", frame.rawFsdState), ("rawFsdMode", frame.rawFsdMode), ("rawFsdStage", frame.rawFsdStage), ("inSupercruise", value.inSupercruise), ("normalBoostAvailable",TryNormalBoostBinding(frame,out _,out _)),("normalBoostInputs",normalBoostInputs),("normalBoostReleased",normalBoostReleased), ("normalBoostPressed",normalBoostPulse is not null),("normalBoostLastTick",lastNormalBoostAt), ("dockingZeroAxisCommits", dockingZeroAxisCommits), ("dockingZeroAxisRestoresSkipped", dockingZeroAxisRestoresSkipped), ("dockingZeroUpdateCalls",dockingZeroUpdateCalls),("dockingZeroUpdateMatched",dockingZeroUpdateMatched),("dockingZeroUpdateApplied",dockingZeroUpdateApplied), ("dockingInputMapperCalls",dockingInputMapperCalls),("dockingInputMapperMatched",dockingInputMapperMatched),("dockingInputMapperApplied",dockingInputMapperApplied), ("dockingInputMapper",Obj(("verified",mapper.verified),("sequence",mapper.sequence),("entityId",mapper.entity.ToString(CultureInfo.InvariantCulture)), ("observedAtUnixMilliseconds",mapper.utc),("accumulatorBefore",mapper.before),("accumulatorAfter",mapper.after),("sourceAfter",mapper.source))), ("supercruiseAssist", Obj(("available", value.cruiseAssistAvailable), ("enabled", value.cruiseAssistEnabled), ("engaged", value.cruiseAssistEngaged), ("matchesTarget", value.cruiseAssistMatches), ("obscured", value.cruiseAssistObscured))), ("docking", Obj(("verified", value.dockingVerified), ("computerAvailable", value.dockingComputerAvailable), ("clearanceGranted", value.dockingGranted), ("assistAllowed", value.dockingAssistAllowed), ("assistEnabled", value.dockingAssistEnabled), ("assistEngaged", value.dockingAssistEngaged), ("nativeDutyActive", value.nativeDockingActive), ("nativeLaunchActive", value.nativeLaunchActive), ("nativeLaunchRequested", value.nativeLaunchRequested), ("throttleZero", value.dockingThrottleZero), ("zeroThrottleInputSent", value.dockingZeroInputSent)))); } private static JsonObject LocalFlightRequest(string op, JsonElement request, string owner, in Snapshot frame) { var now = GetTickCount64(); if (op == "local.suspend") { if (clientOwner != owner) throw new InvalidOperationException("Local control belongs to another controller"); lock (leaseLock) { if (lease.local) lease.expires = 0; if (fsdLease.local) fsdLease.expires = 0; } lock(localLock)dockingZeroUpdate.command.expires=0; return Obj(("suspended", true)); } if (!Enabled() || !inGame || !frame.valid || !frame.healthValid || frame.hullFraction<=0 || frame.healthEmergency || now - frame.tick > 200) throw new InvalidOperationException("Fresh healthy player ship required for local navigation"); ulong expiry; lock (leaseLock) expiry = Math.Max(lease.expires, Math.Max(fsdLease.expires, targetLease.expires)); LocalSelection wanted; lock (localLock) wanted = localSelection; if (expiry <= now && wanted.expires <= now && !fsdOwned && op == "local.select") clientOwner = ""; if (clientOwner.Length != 0 && clientOwner != owner) throw new InvalidOperationException("Another controller owns local navigation"); if (op == "local.select") { if (expiry > now || !LocalSelectionFsdReady(frame.rawFsdState,fsdOwned)) throw new InvalidOperationException("Stop flight control before selecting a local destination"); LocalIdentity identity = new() { key = request.Required("key").GetInt32(), body = ulong.Parse(request.Required("bodyAddress").GetString()!, CultureInfo.InvariantCulture), kind = request.Required("kind").GetUInt32(), market = request.Required("marketId").GetUInt32() }; var name = request.Required("name").GetString()!; var encoded = Encoding.UTF8.GetBytes(name); if (identity.key < 0 || identity.body == 0 || identity.kind == 0 || encoded.Length == 0 || encoded.Length >= 513 || name.Contains('\0')) throw new InvalidOperationException("Invalid local target identity"); encoded.CopyTo(new Span(identity.name, 513)); if (clientOwner.Length == 0) overridden = false; ClaimController(owner); wanted = new() { target = identity, generation = ++localGeneration, expires = now + 300, entity = frame.entity, epoch = controllerEpoch, life = frame.life, fsd = frame.fsd, inputRoot = frame.inputRoot }; lock (localLock) localSelection = wanted; } else if (clientOwner != owner || wanted.generation == 0 || request.Required("generation").GetUInt64() != wanted.generation) throw new InvalidOperationException("No matching local target request"); if (op == "local.action") { var reference = Reference(request.Required("sequence").GetUInt64(), frame, now); var utc = UtcMilliseconds(); var deadline = request.Required("expiresAtUnixMilliseconds").GetUInt64(); var action = request.Required("action").GetString() switch { "requestDocking" => 1u, "enableSupercruiseAssist" => 2u, "cancelSupercruiseAssist" => 3u, "enterSupercruise" => 5u, "leaveSupercruiseSlowly" => 6u, "safeDisengage" => 7u, "launch" => 8u, "cancelLaunch" => 9u, "zeroThrottleForDocking" => 10u, "setNormalThrottleZero" => 11u, "cancelDockingAssist" => 12u, "boostThrusters"=>13u, _ => 0u }; if (action == 0 || deadline <= utc || deadline > utc + 300 || overridden || localConfirmed != wanted.generation) throw new InvalidOperationException("Invalid or expired local action"); var next = new LocalActionRequest { generation = ++localActionGeneration, selection = wanted.generation, expires = Math.Min(now + deadline - utc, reference.tick + 300), sequence = reference.sequence, entity = frame.entity, epoch = controllerEpoch, world = frame.referenceWorld, life = frame.life, inputRoot = frame.inputRoot, action = action }; lock (localLock) localAction = next; return Obj(("queued", true), ("actionGeneration", next.generation)); } if (op == "local.action.status") { var generation = request.Required("actionGeneration").GetUInt64(); LocalActionRequest command; lock (localLock) command = localAction; if (generation != command.generation || command.selection != wanted.generation) throw new InvalidOperationException("No matching local action"); var applied = localActionApplied == generation; var failed = localActionFailed == generation; return Obj(("applied", applied), ("failed", failed || !applied && command.expires <= now), ("pending", !applied && !failed && command.expires > now), ("actionGeneration", generation)); } if (op == "local.axes.release") { lock (leaseLock) if (lease.local) lease.expires = 0; return Obj(("released", true)); } if (op == "local.apply") { RequirePhysicalInputSources(frame,now); var reference = Reference(request.Required("sequence").GetUInt64(), frame, now); var utc = UtcMilliseconds(); var deadline = request.Required("expiresAtUnixMilliseconds").GetUInt64(); if (deadline <= utc || deadline > utc + 300 || overridden || localConfirmed != wanted.generation) throw new InvalidOperationException("Local control frame expired or selection changed"); var values = request.Required("axes"); if (values.GetArrayLength() != 6) throw new InvalidOperationException("Invalid local control axes"); Float6 axes = default; var invalidAxes = false; for (var index = 0; index < 6; index++) { axes[index] = values[index].GetSingle(); invalidAxes |= !float.IsFinite(axes[index]) || Math.Abs(axes[index]) > 1; } var throttle = request.TryGetProperty("throttle", out var throttleValue) ? throttleValue.GetSingle() : -1; if (!float.IsFinite(throttle) || throttle != -1 && (throttle < 0 || throttle > 1) || invalidAxes) throw new InvalidOperationException("Invalid local control axes"); var next = new Lease { expires = Math.Min(now + deadline - utc, reference.tick + 300), sequence = reference.sequence, inputRoot = frame.inputRoot, axes = axes, throttle = throttle, epoch = controllerEpoch, local = true, localKey = wanted.target.key, localKind = wanted.target.kind, localBody = wanted.target.body, localMarket = wanted.target.market, world = frame.referenceWorld, yieldOnDockingGrant = request.Bool("yieldOnDockingGrant") }; if (!LocalControlMatches(next, frame)) throw new InvalidOperationException("Local target changed before applying controls"); lock (leaseLock) { lease = next; if (fsdLease.local && fsdLease.entity == frame.entity && fsdLease.life == frame.life && fsdLease.command == 1 && fsdLease.expires != 0 && frame.rawFsdState == 3) { fsdLease.expires = next.expires; fsdLease.sequence = reference.sequence; } } return Obj(("accepted", true), ("generation", wanted.generation), ("sequence", reference.sequence)); } LocalFlightObservation observed; LocalTargetObservation selected; string stage; lock (localLock) { observed = localObservation; selected = localTargetObservation; stage = localSampleStage; } // The original navigation selection is observable before a remote body's // full flight geometry is ready. Confirm that selection independently; // local.read and all flight actions still require complete telemetry. var confirmed = wanted.entity == frame.entity && wanted.life == frame.life && wanted.fsd == frame.fsd && wanted.inputRoot == frame.inputRoot && LocalSelectionAcknowledged(wanted, selected, localConfirmed, now, controllerEpoch, overridden); var telemetryReady = observed.valid && observed.entity == frame.entity && observed.world == frame.referenceWorld && SameLocal(observed.target, wanted.target) && observed.tick <= now && now - observed.tick <= 200; return Obj(("generation", wanted.generation), ("selected", confirmed), ("pending", !confirmed && localFailed != wanted.generation && wanted.expires > now), ("target", LocalIdentityJson(selected.target)), ("flightInputsSent", 0), ("telemetryReady", telemetryReady), ("sampleStage", stage), ("sampleLimits", Obj(("normalSpeed", double.IsFinite(observed.normalSpeedLimit) ? observed.normalSpeedLimit : 0), ("cruiseSpeed", double.IsFinite(observed.cruiseSpeedLimit) ? observed.cruiseSpeedLimit : 0), ("acceleration", double.IsFinite(observed.accelerationLowerBound) ? observed.accelerationLowerBound : 0)))); } }