#if PLUGIN_CRUISE_OBSERVATION || PLUGIN_CRUISE_CONTROL
using XFEEDToolBox.Core.Navigation;
namespace XFEEDToolBox.Plugin;
internal static unsafe partial class Navigation
{
private static JsonObject? cruiseTelemetryFailure;
internal static JsonObject? CruiseTelemetryFailure()=>cruiseTelemetryFailure?.DeepClone().AsObject();
private static void RefuseCruiseTelemetry(string reason,in Snapshot frame)
{
cruiseTelemetryFailure=Obj(("reason",reason),("observedAtUnixMilliseconds",UtcMilliseconds()),("sampleStage",sampleStage),
("entityId",frame.entity.ToString()),("referenceWorldAddress",frame.referenceWorld.ToString()),
("speedFraction",double.IsFinite(frame.speedFraction)?(double?)frame.speedFraction:null),
("speedMetresPerSecond",double.IsFinite(frame.speed)?(double?)frame.speed:null),("rawFsdState",frame.rawFsdState));
}
private static bool CruiseCurveBound(nuint curve,out OriginalCurveRange range,out int mode,out ulong count)
{
range=default;mode=Read<int>(curve);count=Read<ulong>(curve+0x10);var data=Pointer(curve+0x18);
if(count is 0 or >256||data<0x10000)return false;
var points=new OriginalCurveKnot[(int)count];
for(var i=0;i<points.Length;i++)
{var at=data+(nuint)i*20;points[i]=new(Read<float>(at),Read<float>(at+4),Read<float>(at+12),Read<float>(at+16));}
return OriginalCurveBounds.TryBound(points,mode,0,1,out range);
}
private static bool CruiseFuelBound(in Snapshot frame,nuint root,out double upper,out double multiplier,out double conversion,out double lower)
{
upper=multiplier=conversion=lower=double.NaN;
var fuel=RegisteredComponent(Pointer(frame.life+0xC8)-0x30,Read<uint>(image+0x5F45128));
if(fuel<0x10000||Pointer(fuel)!=image+0x550E8A8||Pointer(root+0x150)!=fuel
||Pointer(root+0x60)!=image+0x533E7A0||Pointer(image+0x533E7A0)!=image+0x2EA9200
||Pointer(Pointer(fuel)+0x18)!=image+0x380A9B0||Read<double>(image+0x534E798)!=OriginalFuelConsumptionBounds.OriginalEnergyConversion)return false;
// Original 2EA64D0 registers root+60 with this fuel interface. Verify
// the existing registration; never add a callback or invoke it here.
var count=Read<ulong>(fuel+0x198);var capacity=Read<ulong>(fuel+0x188);var callbacks=Pointer(fuel+0x1A0);
if(count is 0 or >256||count>capacity||capacity>4096||callbacks<0x10000)return false;
var found=0;for(nuint i=0;i<(nuint)count;i++)if(Pointer(callbacks+i*8)==root+0x60)found++;
if(found!=1)return false;
var provider=Pointer(root+0x258);
if(provider<0x10000||Pointer(provider)!=image+0x532A288||Pointer(Pointer(provider)+0x20)!=image+0x2DCDC40)return false;
// Pure pointer path from original 2DCDC40 -> 1B3AFF0. No virtual
// methods are guessed or called, and low-fuel callback side effects
// in 2EA9200 are never triggered by observation.
var module=Pointer(provider+0x5B8);if(module<0x10000)return false;
var detail=Pointer(module+0x60);if(detail<0x10000)return false;
var inner=Pointer(detail+8)&~(nuint)3;if(inner<0x10000)return false;
var entry=Pointer(inner+0x30);if(entry<0x10000)return false;
var parameters=Pointer(entry+0x30);if(parameters<0x10000)return false;
multiplier=Read<float>(parameters+0x218);conversion=Read<float>(fuel+0x278);
if(!CruiseCurveBound(root+0x2C0,out var curve,out _,out _))return false;
if(!OriginalFuelConsumptionBounds.TryConvertRange(curve,multiplier,conversion,out var range))return false;
lower=range.Minimum;upper=range.Maximum;return true;
}
internal sealed class RecoveryCruiseBinding
{internal Snapshot player;internal nuint component;}
internal static RecoveryCruiseBinding BindRecoveryCruise(ulong expectedEntity)
{
var frame=RecoveryPlayer(expectedEntity);
var component=RegisteredComponent(Pointer(frame.life+0xC8)-0x30,Read<uint>(image+0x5F44D00));
if(component!=0&&Pointer(component)!=image+0x533E6E0)throw new InvalidOperationException("Original main-ship SCO interface type differs");
return new(){player=frame,component=component};
}
internal static bool RecoveryCruiseStopped(RecoveryCruiseBinding binding,ulong expectedWorld)
{
var player=binding.player;var sco=binding.component;
return Sample(player.physics,out var frame)&&frame.entity==player.entity&&frame.life==player.life&&expectedWorld!=0&&frame.referenceWorld==expectedWorld
&&frame.healthValid&&!frame.healthEmergency&&frame.hullFraction>0&&sco>=0x10000&&Pointer(sco)==image+0x533E6E0
&&RegisteredComponent(Pointer(frame.life+0xC8)-0x30,Read<uint>(image+0x5F44D00))==sco&&Read<byte>(sco+0x228) is 0 or 3;
}
internal static JsonObject? ObserveRecoveryCruise(RecoveryCruiseBinding binding,nuint callbackInterface)
{
cruiseTelemetryFailure=null;var player=binding.player;
if(!Sample(player.physics,out var frame)){RefuseCruiseTelemetry("main-physics-sample",frame);return null;}
var changed=frame.entity!=player.entity?"entity":frame.life!=player.life?"life":frame.fsd!=player.fsd?"fsd":
frame.inputRoot!=player.inputRoot?"input-root":!BoundFsd(frame,frame.fsd)?"controlled-main-binding":!frame.healthValid?"ship-health":null;
if(changed is not null){RefuseCruiseTelemetry(changed,frame);return null;}
var sco=RegisteredComponent(Pointer(frame.life+0xC8)-0x30,Read<uint>(image+0x5F44D00));
if(sco!=callbackInterface||sco!=binding.component||sco<0x10000||Pointer(sco)!=image+0x533E6E0||Pointer(sco-0x58)!=image+0x533E5B0
||Pointer(Pointer(sco)+0x20)!=image+0x2EAACD0||Pointer(Pointer(sco)+0x50)!=image+0x2EADB40
||Pointer(Pointer(sco)+0x68)!=image+0x2E970C0){RefuseCruiseTelemetry("original-sco-interface",frame);return null;}
var state=Read<byte>(sco+0x228);if(state>3)throw new InvalidOperationException("Unknown original SCO state");
byte reason=0;var eligible=(byte)Call(image+0x2E970C0,(ulong)sco,(ulong)(nuint)(&reason))!=0;
var fuel=ReadOriginalFuel(frame);var root=sco-0x58;var curve=root+0x2E0;
var curveVerified=CruiseCurveBound(curve,out var curveRange,out var mode,out var count);
var fuelBoundVerified=CruiseFuelBound(frame,root,out var fuelUpper,out var fuelMultiplier,out var fuelConversion,out var fuelLower);
var consumer=Pointer(root+0x168);var consumerTable=consumer>=0x10000?Pointer(consumer):0;
var consumerMethod=InImage(consumerTable)?Pointer(consumerTable+0x50):0;
return Obj(("verified",true),("entityId",frame.entity.ToString()),("referenceWorldAddress",frame.referenceWorld.ToString()),
("observedAtUnixMilliseconds",frame.utcMilliseconds),("rawFsdState",frame.rawFsdState),("inSupercruise",LocalCruiseArrival(frame)!=0),("stableFsd",frame.rawFsdState==2),
("interface",sco.ToString("x")),("vtableRva","533e6e0"),("state",state),("active",state is 1 or 2),("nativeCanEngage",eligible),("nativeRefusalReason",reason),
("heatPercent",HeatHudPercent(frame)),("thermal",ObserveScoThermal(frame,sco)),("hullFraction",frame.hullFraction),("shieldFraction",frame.shieldKnown?(double?)frame.shieldFraction:null),
("fuelVerified",fuel.verified),("fuelMainTonnes",fuel.verified?(double?)fuel.main:null),("fuelCapacityTonnes",fuel.verified?(double?)fuel.capacity:null),
("reserveFuelTonnes",fuel.verified?(double?)fuel.reserve:null),("curveMode",mode),("curvePointCount",count),("curve2E0Verified",curveVerified),
("curve2E0Minimum",curveVerified?(double?)curveRange.Minimum:null),("curve2E0Maximum",curveVerified?(double?)curveRange.Maximum:null),
// 2E0 is heat. Fuel uses the separate original 2C0 curve and
// 380AE40's verified energy-to-tonne conversion.
("consumer168",consumer.ToString("x")),("consumer168TableRva",InImage(consumerTable)?(consumerTable-image).ToString("x"):null),
("consumer168Method50Rva",InImage(consumerMethod)?(consumerMethod-image).ToString("x"):null),
("fuelBoundVerified",fuelBoundVerified),("fuelModuleMultiplier",double.IsFinite(fuelMultiplier)?(double?)fuelMultiplier:null),
("fuelNativeConversion",double.IsFinite(fuelConversion)?(double?)fuelConversion:null),
("fuelRateUpperTonnesPerSecond",fuelBoundVerified?(double?)fuelUpper:null),("fuelRateLowerTonnesPerSecond",fuelBoundVerified?(double?)fuelLower:null),
("fuelRateLowerVerified",fuelBoundVerified&&Read<byte>(image+0x5FB18C9)==0),
("fuelRateBoundsScope","Original SCO energy curve over pressure 0..1, when consuming; excludes other consumers and fuel scooping"),
("overchargeControl",false),("overchargeCommandsSent",0));
}
}
#endif
#if PLUGIN_CRUISE_OBSERVATION || PLUGIN_CRUISE_CONTROL
using XFEEDToolBox.Core.Navigation;
namespace XFEEDToolBox.Plugin;
internal static unsafe partial class Navigation
{
private static JsonObject? cruiseTelemetryFailure;
internal static JsonObject? CruiseTelemetryFailure()=>cruiseTelemetryFailure?.DeepClone().AsObject();
private static void RefuseCruiseTelemetry(string reason,in Snapshot frame)
{
cruiseTelemetryFailure=Obj(("reason",reason),("observedAtUnixMilliseconds",UtcMilliseconds()),("sampleStage",sampleStage),
("entityId",frame.entity.ToString()),("referenceWorldAddress",frame.referenceWorld.ToString()),
("speedFraction",double.IsFinite(frame.speedFraction)?(double?)frame.speedFraction:null),
("speedMetresPerSecond",double.IsFinite(frame.speed)?(double?)frame.speed:null),("rawFsdState",frame.rawFsdState));
}
private static bool CruiseCurveBound(nuint curve,out OriginalCurveRange range,out int mode,out ulong count)
{
range=default;mode=Read<int>(curve);count=Read<ulong>(curve+0x10);var data=Pointer(curve+0x18);
if(count is 0 or >256||data<0x10000)return false;
var points=new OriginalCurveKnot[(int)count];
for(var i=0;i<points.Length;i++)
{var at=data+(nuint)i*20;points[i]=new(Read<float>(at),Read<float>(at+4),Read<float>(at+12),Read<float>(at+16));}
return OriginalCurveBounds.TryBound(points,mode,0,1,out range);
}
private static bool CruiseFuelBound(in Snapshot frame,nuint root,out double upper,out double multiplier,out double conversion,out double lower)
{
upper=multiplier=conversion=lower=double.NaN;
var fuel=RegisteredComponent(Pointer(frame.life+0xC8)-0x30,Read<uint>(image+0x5F45128));
if(fuel<0x10000||Pointer(fuel)!=image+0x550E8A8||Pointer(root+0x150)!=fuel
||Pointer(root+0x60)!=image+0x533E7A0||Pointer(image+0x533E7A0)!=image+0x2EA9200
||Pointer(Pointer(fuel)+0x18)!=image+0x380A9B0||Read<double>(image+0x534E798)!=OriginalFuelConsumptionBounds.OriginalEnergyConversion)return false;
// Original 2EA64D0 registers root+60 with this fuel interface. Verify
// the existing registration; never add a callback or invoke it here.
var count=Read<ulong>(fuel+0x198);var capacity=Read<ulong>(fuel+0x188);var callbacks=Pointer(fuel+0x1A0);
if(count is 0 or >256||count>capacity||capacity>4096||callbacks<0x10000)return false;
var found=0;for(nuint i=0;i<(nuint)count;i++)if(Pointer(callbacks+i*8)==root+0x60)found++;
if(found!=1)return false;
var provider=Pointer(root+0x258);
if(provider<0x10000||Pointer(provider)!=image+0x532A288||Pointer(Pointer(provider)+0x20)!=image+0x2DCDC40)return false;
// Pure pointer path from original 2DCDC40 -> 1B3AFF0. No virtual
// methods are guessed or called, and low-fuel callback side effects
// in 2EA9200 are never triggered by observation.
var module=Pointer(provider+0x5B8);if(module<0x10000)return false;
var detail=Pointer(module+0x60);if(detail<0x10000)return false;
var inner=Pointer(detail+8)&~(nuint)3;if(inner<0x10000)return false;
var entry=Pointer(inner+0x30);if(entry<0x10000)return false;
var parameters=Pointer(entry+0x30);if(parameters<0x10000)return false;
multiplier=Read<float>(parameters+0x218);conversion=Read<float>(fuel+0x278);
if(!CruiseCurveBound(root+0x2C0,out var curve,out _,out _))return false;
if(!OriginalFuelConsumptionBounds.TryConvertRange(curve,multiplier,conversion,out var range))return false;
lower=range.Minimum;upper=range.Maximum;return true;
}
internal sealed class RecoveryCruiseBinding
{internal Snapshot player;internal nuint component;}
internal static RecoveryCruiseBinding BindRecoveryCruise(ulong expectedEntity)
{
var frame=RecoveryPlayer(expectedEntity);
var component=RegisteredComponent(Pointer(frame.life+0xC8)-0x30,Read<uint>(image+0x5F44D00));
if(component!=0&&Pointer(component)!=image+0x533E6E0)throw new InvalidOperationException("Original main-ship SCO interface type differs");
return new(){player=frame,component=component};
}
internal static bool RecoveryCruiseStopped(RecoveryCruiseBinding binding,ulong expectedWorld)
{
var player=binding.player;var sco=binding.component;
return Sample(player.physics,out var frame)&&frame.entity==player.entity&&frame.life==player.life&&expectedWorld!=0&&frame.referenceWorld==expectedWorld
&&frame.healthValid&&!frame.healthEmergency&&frame.hullFraction>0&&sco>=0x10000&&Pointer(sco)==image+0x533E6E0
&&RegisteredComponent(Pointer(frame.life+0xC8)-0x30,Read<uint>(image+0x5F44D00))==sco&&Read<byte>(sco+0x228) is 0 or 3;
}
internal static JsonObject? ObserveRecoveryCruise(RecoveryCruiseBinding binding,nuint callbackInterface)
{
cruiseTelemetryFailure=null;var player=binding.player;
if(!Sample(player.physics,out var frame)){RefuseCruiseTelemetry("main-physics-sample",frame);return null;}
var changed=frame.entity!=player.entity?"entity":frame.life!=player.life?"life":frame.fsd!=player.fsd?"fsd":
frame.inputRoot!=player.inputRoot?"input-root":!BoundFsd(frame,frame.fsd)?"controlled-main-binding":!frame.healthValid?"ship-health":null;
if(changed is not null){RefuseCruiseTelemetry(changed,frame);return null;}
var sco=RegisteredComponent(Pointer(frame.life+0xC8)-0x30,Read<uint>(image+0x5F44D00));
if(sco!=callbackInterface||sco!=binding.component||sco<0x10000||Pointer(sco)!=image+0x533E6E0||Pointer(sco-0x58)!=image+0x533E5B0
||Pointer(Pointer(sco)+0x20)!=image+0x2EAACD0||Pointer(Pointer(sco)+0x50)!=image+0x2EADB40
||Pointer(Pointer(sco)+0x68)!=image+0x2E970C0){RefuseCruiseTelemetry("original-sco-interface",frame);return null;}
var state=Read<byte>(sco+0x228);if(state>3)throw new InvalidOperationException("Unknown original SCO state");
byte reason=0;var eligible=(byte)Call(image+0x2E970C0,(ulong)sco,(ulong)(nuint)(&reason))!=0;
var fuel=ReadOriginalFuel(frame);var root=sco-0x58;var curve=root+0x2E0;
var curveVerified=CruiseCurveBound(curve,out var curveRange,out var mode,out var count);
var fuelBoundVerified=CruiseFuelBound(frame,root,out var fuelUpper,out var fuelMultiplier,out var fuelConversion,out var fuelLower);
var consumer=Pointer(root+0x168);var consumerTable=consumer>=0x10000?Pointer(consumer):0;
var consumerMethod=InImage(consumerTable)?Pointer(consumerTable+0x50):0;
return Obj(("verified",true),("entityId",frame.entity.ToString()),("referenceWorldAddress",frame.referenceWorld.ToString()),
("observedAtUnixMilliseconds",frame.utcMilliseconds),("rawFsdState",frame.rawFsdState),("inSupercruise",LocalCruiseArrival(frame)!=0),("stableFsd",frame.rawFsdState==2),
("interface",sco.ToString("x")),("vtableRva","533e6e0"),("state",state),("active",state is 1 or 2),("nativeCanEngage",eligible),("nativeRefusalReason",reason),
("heatPercent",HeatHudPercent(frame)),("thermal",ObserveScoThermal(frame,sco)),("hullFraction",frame.hullFraction),("shieldFraction",frame.shieldKnown?(double?)frame.shieldFraction:null),
("fuelVerified",fuel.verified),("fuelMainTonnes",fuel.verified?(double?)fuel.main:null),("fuelCapacityTonnes",fuel.verified?(double?)fuel.capacity:null),
("reserveFuelTonnes",fuel.verified?(double?)fuel.reserve:null),("curveMode",mode),("curvePointCount",count),("curve2E0Verified",curveVerified),
("curve2E0Minimum",curveVerified?(double?)curveRange.Minimum:null),("curve2E0Maximum",curveVerified?(double?)curveRange.Maximum:null),
// 2E0 is heat. Fuel uses the separate original 2C0 curve and
// 380AE40's verified energy-to-tonne conversion.
("consumer168",consumer.ToString("x")),("consumer168TableRva",InImage(consumerTable)?(consumerTable-image).ToString("x"):null),
("consumer168Method50Rva",InImage(consumerMethod)?(consumerMethod-image).ToString("x"):null),
("fuelBoundVerified",fuelBoundVerified),("fuelModuleMultiplier",double.IsFinite(fuelMultiplier)?(double?)fuelMultiplier:null),
("fuelNativeConversion",double.IsFinite(fuelConversion)?(double?)fuelConversion:null),
("fuelRateUpperTonnesPerSecond",fuelBoundVerified?(double?)fuelUpper:null),("fuelRateLowerTonnesPerSecond",fuelBoundVerified?(double?)fuelLower:null),
("fuelRateLowerVerified",fuelBoundVerified&&Read<byte>(image+0x5FB18C9)==0),
("fuelRateBoundsScope","Original SCO energy curve over pressure 0..1, when consuming; excludes other consumers and fuel scooping"),
("overchargeControl",false),("overchargeCommandsSent",0));
}
}
#endif