#if PLUGIN_CRUISE_CONTROL
using System.IO.Pipes;
using System.Text;
using XFEEDToolBox.Core.Navigation;
namespace XFEEDToolBox.Plugin;
// The only game mutation is the verified original SCO enable/disable method,
// dispatched from the original main-ship update on its owning thread.
internal static class CruiseControlRuntime
{
private const nuint Update=0x2EADB40,OriginalSet=0x2EAACD0;
private const string Service=CruiseControlProtocol.Service;
private static bool usingLegacyCommandObserver;
private sealed record Intent(string Owner,string RequestId,ulong Generation,ulong SelectionGeneration,ulong Deadline,JsonElement Target,double Reserve);
private static readonly object sync=new();
private static bool started,enabled,owned,releaseRequested,awaitingOff,damageSeen;
private static nuint image,original,originalSet;
[ThreadStatic]private static int ownedSetDepth;
[ThreadStatic]private static int originalUpdateDepth;
private static readonly Queue<JsonObject> originalCommands=new();
private static ulong originalCommandSerial;
private static bool replacedLegacyUpdate;
internal static void EnterOwnedOriginalCommand()=>ownedSetDepth++;
internal static void ExitOwnedOriginalCommand()=>ownedSetDepth--;
private static uint thread;
private static Navigation.RecoveryCruiseBinding? binding;
private static Intent? intent;
private static string activeOwner="";
private static string activeStartRequestId="",lastLeaseRequestId="",releaseRequestId="";
private static ulong activeSelectionGeneration;
private static ulong entity,world,calls,matches,errors,lastSampleTick,generation,consumed,applied,failed,enableCalls,disableCalls,activeSamples,offSamples;
private static readonly ScoHeatSampleHistory heatHistory=new();
private static readonly ScoExitSimulationClock exitClock=new();
private static readonly ScoExitEstimatePolicy exitPolicy=new();
private static double previousHull=double.NaN,previousShield=double.NaN,heatSlope=double.NaN;
private static double simulationStepSeconds=double.NaN;
private static double originalUpdateThirdArgument=double.NaN;
private static ulong lastOriginalStopCallMilliseconds;
private static JsonObject? latest,receipt,lastOriginalStop,lastGate;
private static readonly OriginalCruiseCycleQualification qualification=new();
private static string? issue;
private static bool Key(string value)=>value.Length==32&&value.All(c=>c is >= '0' and <= '9' or >= 'a' and <= 'f');
private static bool Fresh(ulong at,ulong now)=>at<=now&&now-at<=200;
private static void ObserveHeat(ulong at,double heat)
{
heatHistory.Observe(at,heat);heatSlope=heatHistory.Slope;
}
private static double GuardHeatRise(ulong at)=>heatHistory.ConservativeSlope(at);
private static JsonObject HeatObservation(ulong at)=>Obj(("samplingPhase","before-original-update"),("usedForControl",false),("slopePercentPerSecond",heatSlope),
("windowStartUnixMilliseconds",heatHistory.SlopeStartAt),("windowEndUnixMilliseconds",heatHistory.SlopeEndAt),
("windowMilliseconds",heatHistory.SlopeEndAt-heatHistory.SlopeStartAt),("windowStartHeatPercent",heatHistory.SlopeStartHeat),
("windowEndHeatPercent",heatHistory.SlopeEndHeat),("recentPeakPercentPerSecond",heatHistory.RecentPeakRise),("recentPeakExpiresAtUnixMilliseconds",heatHistory.RecentPeakExpiresAt),
("guardHeatRisePercentPerSecond",GuardHeatRise(at)));
private static bool Flag(JsonObject value,string key)=>value[key] is JsonValue v&&v.TryGetValue<bool>(out var flag)&&flag;
private static double Number(JsonObject value,string key)
{
if(value[key] is not JsonValue v)return double.NaN;
if(v.TryGetValue<double>(out var d))return d;if(v.TryGetValue<float>(out var f))return f;
if(v.TryGetValue<int>(out var i))return i;if(v.TryGetValue<uint>(out var u))return u;
if(v.TryGetValue<byte>(out var b))return b;if(v.TryGetValue<ulong>(out var n))return n;
return double.NaN;
}
private static void AttachThermalTiming(JsonObject sample)
{
sample["originalUpdateClockEpoch"]=exitClock.Epoch;sample["originalUpdateCompletedSteps"]=exitClock.CompletedSteps;
sample["originalUpdateCompletedSimulationSeconds"]=exitClock.Valid?(double?)exitClock.CompletedSeconds:null;
sample["originalUpdateClockValid"]=exitClock.Valid;
sample["originalUpdateClockScope"]="Sum of every completed matched original SCO callback delta; no sparse IPC dt or UTC substitution";
sample["scoExitApproach"]=exitPolicy.Latest.Json();
sample["sameFrameSimulationStepSeconds"]=double.IsFinite(simulationStepSeconds)?(double?)simulationStepSeconds:null;
sample["sameFrameSimulationStepSource"]="owning original SCO update delta argument";
sample["originalUpdateThirdArgument"]=double.IsFinite(originalUpdateThirdArgument)?(double?)originalUpdateThirdArgument:null;
sample["originalUpdateThirdArgumentMeaningVerified"]=false;
if(sample["thermal"] is not JsonObject thermal)return;
var heatStep=Number(thermal,"nativeHeatStepSeconds");
var response=Math.Max(simulationStepSeconds,heatStep);
thermal["responseDelaySeconds"]=double.IsFinite(response)&&response is >0 and <=.25?(double?)response:null;
thermal["afterStopObservationSeconds"]=.5;
thermal["heatNumericalUncertaintyPercent"]=.001;
thermal["stopCallWallMilliseconds"]=lastOriginalStopCallMilliseconds;
thermal["currentSimulationStepSeconds"]=double.IsFinite(simulationStepSeconds)?(double?)simulationStepSeconds:null;
thermal["wallLeaseIsSimulationBound"]=false;
thermal["timingBasis"]="Current actual simulation dt is guarded before original Active heat input; UTC lease is not a proven cumulative simulation horizon; synchronous stop wall time does not continue SCO heating";
}
private static ScoThermalInput? ThermalInput(JsonObject sample,ulong now,ulong deadline)
{
if(sample["thermal"] is not JsonObject thermal)return null;
return new(){Verified=Flag(thermal,"verified")&&thermal["entityId"]?.ToString()==entity.ToString()
&&thermal["referenceWorldAddress"]?.ToString()==world.ToString(),
HeatPercent=Number(sample,"heatPercent"),HeatCapacity=Number(thermal,"heatCapacity"),HeatDisplayScale=Number(thermal,"heatDisplayScale"),
ScoInputUpper=Number(thermal,"scoInputUpper"),OtherInputUpper=Number(thermal,"otherInputUpper"),
CoolingPassive=Number(thermal,"coolingPassive"),CoolingMaximum=Number(thermal,"coolingMaximum"),CoolingScale=Number(thermal,"coolingScale"),
CoolingHeatLower=Number(thermal,"coolingHeatLower"),CoolingHeatUpper=Number(thermal,"coolingHeatUpper"),
SourceAgeSeconds=(now-Number(thermal,"sourceObservedAtUnixMilliseconds"))/1000d,
HeatAgeSeconds=(now-Number(thermal,"heatObservedAtUnixMilliseconds"))/1000d,
LeaseRemainingSeconds=deadline>now?(deadline-now)/1000d:0,ResponseDelaySeconds=Number(thermal,"responseDelaySeconds"),
AfterStopSeconds=.5,HeatUncertaintyPercent=.001};
}
private static bool CurrentSimulationHeatBound(JsonObject sample,ScoThermalInput? model,ulong now,out double peak)
{
peak=double.NaN;if(model is null||sample["thermal"] is not JsonObject thermal)return false;
var storedHeat=Number(thermal,"currentStoredEnergyHeatPercent");var storedAt=Number(thermal,"currentStoredEnergyObservedAtUnixMilliseconds");
if(!double.IsFinite(storedHeat)||storedHeat<0||!double.IsFinite(storedAt)||storedAt>now||now-storedAt>200)return false;
var heat=Math.Max(Number(sample,"heatPercent"),storedHeat);
return ScoThermalPredictor.TryBoundCurrentSimulationStep(model with{HeatPercent=heat,HeatAgeSeconds=0},simulationStepSeconds,out peak);
}
private static JsonObject? ThermalForecast(ScoThermalForecast? forecast)=>forecast is null?null:Obj(("known",forecast.Known),
("scoOnNetRisePercentPerSecond",double.IsFinite(forecast.ScoOnNetRisePercentPerSecond)?(double?)forecast.ScoOnNetRisePercentPerSecond:null),
("scoOffNetRisePercentPerSecond",double.IsFinite(forecast.ScoOffNetRisePercentPerSecond)?(double?)forecast.ScoOffNetRisePercentPerSecond:null),
("secondsTo100LowerBound",forecast.SecondsTo100LowerBound),
("continueUntilDeadlinePeakPercent",double.IsFinite(forecast.ContinueUntilDeadlinePeakPercent)?(double?)forecast.ContinueUntilDeadlinePeakPercent:null),
("deadlineStopPeakPercent",double.IsFinite(forecast.DeadlineStopPeakPercent)?(double?)forecast.DeadlineStopPeakPercent:null),
("immediateStopPeakPercent",double.IsFinite(forecast.ImmediateStopPeakPercent)?(double?)forecast.ImmediateStopPeakPercent:null),
("immediateStopEndPercent",double.IsFinite(forecast.ImmediateStopEndPercent)?(double?)forecast.ImmediateStopEndPercent:null),
("deadlineStopEndPercent",double.IsFinite(forecast.DeadlineStopEndPercent)?(double?)forecast.DeadlineStopEndPercent:null),
("stopHorizonSeconds",forecast.StopHorizonSeconds),("shouldStop",forecast.ShouldStop),("reason",forecast.Reason));
private static async Task<JsonElement> NavAsync(JsonObject request,string service="Navigation")
{
using var timeout=new CancellationTokenSource(TimeSpan.FromMilliseconds(150));
using var pipe=new NamedPipeClientStream(".",$"XFEEDToolBox.{service}.{ProcessId}",PipeDirection.InOut,PipeOptions.Asynchronous);
await pipe.ConnectAsync(timeout.Token);await pipe.WriteAsync(Encoding.UTF8.GetBytes(request.ToJsonString()+"\n"),timeout.Token);await pipe.FlushAsync(timeout.Token);
var bytes=new byte[65536];var count=0;
while(count<bytes.Length){var n=await pipe.ReadAsync(bytes.AsMemory(count),timeout.Token);if(n==0)break;count+=n;if(bytes[count-1]=='\n')break;}
if(count is 0 or 65536)throw new IOException("Invalid original navigation response.");
using var document=JsonDocument.Parse(bytes.AsMemory(0,count));var response=document.RootElement;
if(!response.GetProperty("ok").GetBoolean())throw new InvalidOperationException(response.Str("error"));
return response.GetProperty("result").Clone();
}
private static JsonElement Nav(JsonObject request)=>NavAsync(request).GetAwaiter().GetResult();
private static bool SameTarget(JsonElement a,JsonElement b)=>a.Int("key")==b.Int("key")&&a.Str("bodyAddress")==b.Str("bodyAddress")
&&a.U64("kind")==b.U64("kind")&&a.U64("marketId")==b.U64("marketId")&&a.Str("name")==b.Str("name");
private static JsonObject Describe()
{
lock(sync)
{
var observed=latest is null?0:(ulong)Number(latest,"observedAtUnixMilliseconds");
return Obj(("schema",1),("service",Service),("version",Platform.PluginVersion),("processId",ProcessId),("gameSha256",GameHash),("enabled",enabled),("owningThread",thread),
("replacedIdleLegacyUpdate",replacedLegacyUpdate),("usingReadOnlyLegacyCommandObserver",usingLegacyCommandObserver),
("watchEntityId",entity.ToString()),("watchReferenceWorld",world.ToString()),("watchInterface",binding?.component.ToString("x")),
("sampleReady",observed!=0&&Fresh(observed,Utc())),("sample",latest?.DeepClone()),("overchargeControl",true),
("originalFuelRangeObservations",true),("componentThermalModel",true),("nativeStopReceipts",true),("expiringOriginalControl",true),("controlTelemetrySchema",1),
("overchargeControlQualified",qualification.Qualified),("cycleActiveSamples",qualification.ActiveSamples),("qualifiedCycle",QualifiedCycle()),
("owned",owned),("leaseExpiresAtUnixMilliseconds",intent?.Deadline??0),("receipt",receipt?.DeepClone()),("lastOriginalStop",lastOriginalStop?.DeepClone()),("lastGate",lastGate?.DeepClone()),
("scoExitEstimate",exitPolicy.Status()),
("originalCommandObservations",new JsonArray(originalCommands.Select(value=>(JsonNode)value.DeepClone()).ToArray())),("originalCommandSerial",originalCommandSerial),
("generation",generation),("consumed",consumed),("applied",applied),("failed",failed),
("calls",calls),("matches",matches),("errors",errors),("issue",issue),("originalEnableCalls",enableCalls),("originalDisableCalls",disableCalls),
("actualActiveSamples",activeSamples),("actualOffSamples",offSamples),("overchargeCommandsSent",enableCalls+disableCalls),
("steeringInputsSent",0),("targetSelectionsSent",0));
}
}
private static JsonObject? QualifiedCycle()
{
var proof=qualification.Completed;if(proof is null)return null;var identity=proof.Identity;
return Obj(("owner",identity.Owner),("startRequestId",identity.StartRequestId),("selectionGeneration",identity.SelectionGeneration),
("entityId",identity.Entity.ToString()),("referenceWorldAddress",identity.World.ToString()),("activeSamples",proof.ActiveSamples),
("enableAtUnixMilliseconds",proof.EnableAt),("lastActiveAtUnixMilliseconds",proof.LastActiveAt),("releaseRequestId",proof.StopRequestId),
("originalStopAtUnixMilliseconds",proof.StopAt),("offAtUnixMilliseconds",proof.OffAt));
}
private static JsonNode Handle(JsonElement request)
{
var op=request.Str("op");if(op is "describe" or "read")return Describe();
if(op=="sco.exit.configure")
{
lock(sync)
{
if(request.Str("mode")=="shadow"){exitPolicy.ShadowOnly();return Describe();}
var mode=request.Str("mode");
if(mode is not("trial" or "production")||owned||intent is not null)throw new InvalidOperationException("Exit strategy configuration requires an idle owned-command channel.");
var expectedEntity=request.U64("entityId");var expectedWorld=request.U64("referenceWorldAddress");
string why;
var accepted=mode=="production"
?exitPolicy.PromoteProduction(expectedEntity,expectedWorld,Utc(),request.U64("expiresAtUnixMilliseconds"),out why)
:exitPolicy.ArmTrial(expectedEntity,expectedWorld,Utc(),request.U64("expiresAtUnixMilliseconds"),request.Int("maximumBursts"),out why);
if(!accepted)throw new InvalidOperationException("SCO exit strategy refused: "+why);
return Describe();
}
}
if(op=="deactivate")
{
lock(sync){if(owned||releaseRequested)throw new InvalidOperationException("Release the owned SCO and observe its original stopped state first.");enabled=false;}
HookDisable(image+Update);return Describe();
}
if(!enabled)throw new InvalidOperationException("Original cruise controller is inactive.");
if(op=="watch")
{
var fingerprint=request.Str("loadoutFingerprint");
if(fingerprint.Length!=0&&(fingerprint.Length!=64||!fingerprint.All(Uri.IsHexDigit)))throw new InvalidOperationException("Trusted SHA256 loadout fingerprint required.");
lock(sync)if(owned)throw new InvalidOperationException("Cannot rebind an owned SCO lease.");
var frame=Nav(Obj(("op","read")));var health=frame.GetProperty("shipHealth");var now=Utc();
if(!frame.Bool("inGame")||!frame.Bool("localPlayerShipVerified")||!health.Bool("verified")||!Fresh(frame.U64("observedAtUnixMilliseconds"),now)
||frame.Str("entityId")!=health.Str("entityId"))throw new InvalidOperationException("Fresh original main ship required.");
var next=ulong.Parse(frame.Str("entityId"));var reference=ulong.Parse(frame.Str("referenceWorldAddress"));var selected=Navigation.BindRecoveryCruise(next);
lock(sync)
{
if(owned)throw new InvalidOperationException("Cannot rebind an owned SCO lease.");
if(entity!=next||world!=reference||binding?.component!=selected.component)
{latest=lastGate=null;heatHistory.Reset();exitClock.Reset();exitPolicy.ResetBinding();previousHull=previousShield=heatSlope=double.NaN;damageSeen=false;intent=null;releaseRequested=awaitingOff=false;qualification.ForgetCurrent();}
exitPolicy.BindLoadout(fingerprint);
entity=next;world=reference;binding=selected;issue=selected.component==0?"current-entity-has-no-SCO-interface":"awaiting-original-main-ship-update";
}
return Describe();
}
if(op=="cruise.release")
{
var owner=request.Str("owner");var stopId=request.Str("requestId");if(!Key(owner)||!Key(stopId))throw new InvalidOperationException("Explicit SCO owner and stop request required.");
lock(sync)
{
if(owned&&activeOwner!=owner||intent is not null&&intent.Owner!=owner)throw new InvalidOperationException("SCO belongs to another controller.");
if(owned){releaseRequestId=stopId;releaseRequested=true;}consumed=generation;intent=null;
return Obj(("requestId",stopId),("released",!owned),("pending",owned),("originalDisableCalls",disableCalls),("lastOriginalStop",lastOriginalStop?.DeepClone()));
}
}
if(op!="cruise.set")throw new InvalidOperationException("Unknown original cruise command.");
var ownerId=request.Str("owner");var requestId=request.Str("requestId");var deadline=request.U64("expiresAtUnixMilliseconds");var sequence=request.U64("sequence");
var reserve=request.Num("requiredFuelReserveTonnes",double.NaN);var selection=request.U64("selectionGeneration");var began=Utc();
if(!Key(ownerId)||!Key(requestId)||selection==0||sequence==0||deadline<=began||deadline-began>250||!double.IsFinite(reserve)||reserve<=0)
throw new InvalidOperationException("A bounded original SCO request is required.");
lock(sync)
{
if(intent?.RequestId==requestId)return Describe();
if(owned&&activeOwner!=ownerId||intent is not null&&intent.Owner!=ownerId&&(owned||intent.Deadline>began))throw new InvalidOperationException("Another controller owns SCO.");
if(owned&&activeSelectionGeneration!=selection)throw new InvalidOperationException("Release SCO before changing its original target selection.");
if(intent is not null&&consumed<intent.Generation&&intent.Deadline>began)throw new InvalidOperationException("Previous SCO request is still pending.");
if(!owned&&latest is not null&&Number(latest,"state") is 1 or 2)throw new InvalidOperationException("SCO was already active outside this controller.");
}
var raw=Nav(Obj(("op","read")));var local=Nav(Obj(("op","local.read")));
var status=Nav(Obj(("op","local.status"),("owner",ownerId),("generation",selection)));var at=Utc();
var healthNow=raw.GetProperty("shipHealth");var referenceTarget=request.Required("target");
if(!raw.Bool("inGame")||!raw.Bool("localPlayerShipVerified")||raw.Bool("playerOverride")||!healthNow.Bool("verified")||healthNow.Bool("emergency")
||!Fresh(raw.U64("observedAtUnixMilliseconds"),at)||!Fresh(local.U64("observedAtUnixMilliseconds"),at)||deadline<=at||deadline>local.U64("observedAtUnixMilliseconds")+250
||!status.Bool("selected")||!status.Bool("telemetryReady")||!local.Bool("inSupercruise")||local.Int("rawFsdState")!=2
||!SameTarget(referenceTarget,local.Required("target"))||!SameTarget(referenceTarget,status.Required("target"))
||sequence>local.U64("sequence")||local.U64("sequence")-sequence>16||raw.Str("entityId")!=local.Str("entityId")
||raw.Str("referenceWorldAddress")!=local.Str("referenceWorldAddress"))throw new InvalidOperationException("Original target owner or fresh SC reference changed.");
lock(sync)
{
if(!enabled||binding?.component is null or 0||raw.Str("entityId")!=entity.ToString()||raw.Str("referenceWorldAddress")!=world.ToString()
||owned&&activeOwner!=ownerId||intent is not null&&intent.Owner!=ownerId&&owned||deadline<=Utc())throw new InvalidOperationException("Current main-ship SCO binding changed.");
intent=new(ownerId,requestId,++generation,selection,deadline,local.Required("target").Clone(),reserve);releaseRequested=false;
receipt=Obj(("owner",ownerId),("requestId",requestId),("generation",generation),("selectionGeneration",selection),("queued",true),("applied",false),("nativeMethodCalled",false));return Describe();
}
}
private static void StopOwned(Navigation.RecoveryCruiseBinding selected,nuint sco,string reason,JsonObject? telemetryFailure=null,JsonObject? gate=null)
{
if(!owned)return;
var callStarted=Utc();
if(!Navigation.SetOriginalCruise(selected,sco,false,out var state,out var called,out var before)){issue="original-disable-not-confirmed:"+reason;return;}
lastOriginalStopCallMilliseconds=Utc()-callStarted;
exitPolicy.OriginalStopped(called,before,state);
var stoppedAt=Utc();qualification.Stop(new(activeOwner,activeStartRequestId,activeSelectionGeneration,entity,world),releaseRequestId,stoppedAt,called,before,state,reason=="released");
lastOriginalStop=Obj(("owner",activeOwner),("startRequestId",activeStartRequestId),("leaseRequestId",lastLeaseRequestId),
("selectionGeneration",activeSelectionGeneration),("releaseRequestId",releaseRequestId.Length==0?null:releaseRequestId),("entityId",entity.ToString()),("referenceWorldAddress",world.ToString()),
("nativeMethodRva",called?"2eaacd0":null),("nativeMethodCalled",called),("argument",false),("stateBefore",before),("stateAfter",state),
("reason",reason),("telemetryFailure",telemetryFailure?.DeepClone()),("gate",gate?.DeepClone()),("stopCallWallMilliseconds",lastOriginalStopCallMilliseconds),("observedAtUnixMilliseconds",stoppedAt),("owningThread",GetCurrentThreadId()));
if(called)disableCalls++;owned=false;activeOwner="";releaseRequested=false;awaitingOff=true;issue=reason;
}
private static void PollControl(Navigation.RecoveryCruiseBinding selected,nuint sco)
{
lock(sync)
{
var wanted=intent;var now=Utc();
if(releaseRequested||wanted is null||wanted.Deadline<=now)
{StopOwned(selected,sco,releaseRequested?"released":wanted is null?"no-lease":"lease-expired");wanted=null;}
// Derive heat only here, once per original update. Mixing before/after
// readings divides a whole simulation heat step by a partial wall-clock
// interval and can retain that artificial peak for the next 250 ms.
// Continue sampling without a lease so starting requires a real baseline.
var sample=Navigation.ObserveCruiseControl(selected,sco,wanted?.Target,thread,world);
if(sample is not null)
{
exitPolicy.Observe(ScoExitNativeFrame.Read(sample,exitClock.Epoch,exitClock.CompletedSteps,exitClock.CompletedSeconds,exitClock.Valid,simulationStepSeconds));
AttachThermalTiming(sample);ObserveHeat((ulong)Number(sample,"observedAtUnixMilliseconds"),Number(sample,"heatPercent"));
}
else{heatHistory.Reset();heatSlope=double.NaN;}
if(wanted is null)return;
string? reason="native-telemetry";
JsonObject? gateObservation=null;
if(sample is not null)
{
// Sampling stamps the actual frame. Evaluate its age only after
// that sampling has finished; the pre-sample clock is older.
now=Utc();
var at=(ulong)Number(sample,"observedAtUnixMilliseconds");var heat=Number(sample,"heatPercent");
var hull=Number(sample,"hullFraction");var shield=Number(sample,"shieldFraction");
if(double.IsFinite(previousHull)&&hull<previousHull-1e-6||double.IsFinite(previousShield)&&shield<previousShield-1e-6)damageSeen=true;
previousHull=hull;previousShield=shield;
var state=(int)Number(sample,"state");
var gate=new CruiseOverchargeGateFrame{IdentityVerified=Flag(sample,"verified"),GeometryVerified=Flag(sample,"geometryVerified"),
TargetVerified=Flag(sample,"targetVerified"),InSupercruise=Flag(sample,"inSupercruise"),RawFsdState=(int)Number(sample,"rawFsdState"),
AssistEnabled=Flag(sample,"nativeAssistEnabled"),CollisionCourse=Flag(sample,"collisionCourse"),OverchargeState=state,
Emergency=Flag(sample,"healthEmergency")||Flag(sample,"playerOverride")||damageSeen&&double.IsFinite(shield)&&shield<=1e-6,
NativeCanEngage=Flag(sample,"nativeCanEngage"),ObservedAt=at,Hull=hull,Heat=heat,
HeatRise=GuardHeatRise(at),ThermalInput=ThermalInput(sample,now,wanted.Deadline),
MainFuel=Number(sample,"fuelMainTonnes"),FuelCapacity=Number(sample,"fuelCapacityTonnes"),FuelRateUpper=Flag(sample,"fuelBoundVerified")?Number(sample,"fuelRateUpperTonnesPerSecond"):double.NaN,
RequiredFuelReserve=wanted.Reserve,Distance=Number(sample,"distanceMetres"),Speed=Number(sample,"speedMetresPerSecond"),AimErrorDegrees=Number(sample,"aimErrorDegrees"),
CruiseParametersVerified=Flag(sample,"cruiseParametersVerified"),CruiseApproachSeconds=Number(sample,"cruiseApproachSeconds")};
gate=gate with{ShipEntity=entity,ReferenceWorld=world,TargetKey=wanted.Target.Int("key"),ExitApproach=exitPolicy.Latest};
reason=CruiseOverchargeNativeGuard.Refusal(gate,now,wanted.Deadline);
// This guard covers the exact incoming simulation step, independent of UTC-to-simulation clock mapping.
// Do not spend cooling from a later HeatTick before this Active call has submitted its gross heat input.
var frameHeatKnown=CurrentSimulationHeatBound(sample,gate.ThermalInput,now,out var frameHeatPeak);
if(reason is null&&!frameHeatKnown)reason="thermal-model";
if(reason is null&&frameHeatPeak>=100)reason="heat";
var thermalForecast=gate.ThermalInput is {} thermal?ScoThermalPredictor.Predict(thermal):null;
gateObservation=Obj(("sampleObservedAtUnixMilliseconds",at),("evaluatedAtUnixMilliseconds",now),("leaseExpiresAtUnixMilliseconds",wanted.Deadline),
("heatPercent",gate.Heat),("heatRisePercentPerSecond",gate.HeatRise),
("predictedHeatPercent",thermalForecast is {Known:true}?thermalForecast.DeadlineStopPeakPercent:(double?)null),
("ceilingHeatPercent",100),("thermalModel",sample["thermal"]?.DeepClone()),("thermalForecast",ThermalForecast(thermalForecast)),
("currentFrameGrossHeatBoundKnown",frameHeatKnown),("currentFrameGrossHeatPeakPercent",frameHeatKnown?(double?)frameHeatPeak:null),
("currentSimulationStepSeconds",double.IsFinite(simulationStepSeconds)?(double?)simulationStepSeconds:null),
("currentFrameHeatBoundScope","Full-pressure SCO input and recent other-source envelope over this actual simulation dt; no future radiator credit; other future sources are not an absolute bound"),
("identityVerified",gate.IdentityVerified),("targetVerified",gate.TargetVerified),("geometryVerified",gate.GeometryVerified),
("inSupercruise",gate.InSupercruise),("rawFsdState",gate.RawFsdState),("nativeCanEngage",gate.NativeCanEngage),("nativeRefusalReason",Number(sample!,"nativeRefusalReason")),
("assistEnabled",gate.AssistEnabled),("collisionCourse",gate.CollisionCourse),("emergency",gate.Emergency),
("overchargeState",state),("hullFraction",hull),("shieldFraction",shield),("damageSeen",damageSeen),
("distanceMetres",gate.Distance),("speedMetresPerSecond",gate.Speed),("aimErrorDegrees",gate.AimErrorDegrees),
("cruiseParametersVerified",gate.CruiseParametersVerified),("cruiseApproachSeconds",double.IsFinite(gate.CruiseApproachSeconds)?(double?)gate.CruiseApproachSeconds:null),
("approachStopHorizonSeconds",ScoApproachEnvelope.StopHorizonSeconds(gate.CruiseParametersVerified,gate.CruiseApproachSeconds)),
("scoExitApproach",exitPolicy.Latest.Json()),
("mainFuelTonnes",gate.MainFuel),("fuelCapacityTonnes",gate.FuelCapacity),("requiredFuelReserveTonnes",gate.RequiredFuelReserve),
("fuelRateUpperTonnesPerSecond",gate.FuelRateUpper),("heatObservation",HeatObservation(at)));
lastGate=gateObservation;
if(!owned&&state is 1 or 2)reason="foreign-active";
}
if(reason is not null)
{
StopOwned(selected,sco,reason,sample is null?Navigation.CruiseTelemetryFailure():null,gateObservation);issue="interlock:"+reason;
if(consumed<wanted.Generation)
{
consumed=failed=wanted.Generation;receipt=Obj(("owner",wanted.Owner),("requestId",wanted.RequestId),("generation",wanted.Generation),
("selectionGeneration",wanted.SelectionGeneration),("applied",false),("failed",true),("reason",reason),("nativeMethodCalled",false),
("nativeObservedAtUnixMilliseconds",sample is null?null:(ulong?)Number(sample,"observedAtUnixMilliseconds")),
("evaluatedAtUnixMilliseconds",now),("expiresAtUnixMilliseconds",wanted.Deadline),("gate",gateObservation?.DeepClone()));
}
return;
}
if(consumed>=wanted.Generation)return;
if(wanted.Deadline<=Utc())
{consumed=failed=wanted.Generation;StopOwned(selected,sco,"expired-before-original-call");receipt=Obj(("requestId",wanted.RequestId),("generation",wanted.Generation),("applied",false),("failed",true),("reason","expired-before-original-call"),("nativeMethodCalled",false));return;}
var called=false;var actualState=(byte)Number(sample!,"state");
if(!owned)
{
if(!Navigation.SetOriginalCruise(selected,sco,true,out actualState,out called,out var before)){consumed=failed=wanted.Generation;issue="original-enable-not-confirmed";return;}
if(called)enableCalls++;owned=true;activeOwner=wanted.Owner;activeSelectionGeneration=wanted.SelectionGeneration;activeStartRequestId=wanted.RequestId;releaseRequestId="";
exitPolicy.BeginOwnedBurst(called,before,actualState);
qualification.Begin(new(activeOwner,activeStartRequestId,activeSelectionGeneration,entity,world),Utc(),called,actualState);
}
lastLeaseRequestId=wanted.RequestId;
consumed=applied=wanted.Generation;receipt=Obj(("owner",wanted.Owner),("requestId",wanted.RequestId),("generation",wanted.Generation),("selectionGeneration",wanted.SelectionGeneration),
("entityId",entity.ToString()),("referenceWorldAddress",world.ToString()),("queued",false),("applied",true),
("nativeMethodCalled",called),("nativeMethodRva",called?"2eaacd0":null),("argument",called?(bool?)true:null),("actualStateAfterCall",actualState),
("observedAtUnixMilliseconds",Utc()),("owningThread",GetCurrentThreadId()));issue=null;
}
}
// Observe all actual original commands, including engine-side cancellation.
// The original method is always forwarded exactly once; this hook decides nothing.
[UnmanagedCallersOnly]private static unsafe void ObserveOriginalCommand(nuint sco,byte engage)
{
Navigation.RecoveryCruiseBinding? selected=null;JsonObject? before=null;string source="game-other";ulong began=0;
try
{
lock(sync)
{
if(enabled&&GetCurrentThreadId()==thread&&binding?.component==sco)
{selected=binding;source=ownedSetDepth>0?"toolbox-original-command":originalUpdateDepth>0?"original-sco-update":"game-other";began=Utc();}
}
if(selected is not null)before=Navigation.ObserveRecoveryCruise(selected,sco);
}
catch{/* Diagnostics cannot prevent forwarding the original call. */}
try{Call(originalSet,sco,engage);}
catch{return;}
if(selected is null)return;
try
{
var after=Navigation.ObserveRecoveryCruise(selected,sco);
lock(sync)
{
originalCommands.Enqueue(Obj(("serial",++originalCommandSerial),("source",source),("argument",engage!=0),
("owner",owned?activeOwner:intent?.Owner),("startRequestId",activeStartRequestId),("leaseRequestId",intent?.RequestId),
("selectionGeneration",intent?.SelectionGeneration??activeSelectionGeneration),("entityId",entity.ToString()),("referenceWorldAddress",world.ToString()),
("observedAtUnixMilliseconds",began),("returnedAtUnixMilliseconds",Utc()),("owningThread",GetCurrentThreadId()),
("stateBefore",before is null?null:Number(before,"state")),("stateAfter",after is null?null:Number(after,"state")),
("nativeCanEngageBefore",before is null?null:(bool?)Flag(before,"nativeCanEngage")),
("nativeRefusalReasonBefore",before is null?null:Number(before,"nativeRefusalReason")),
("nativeCanEngageAfter",after is null?null:(bool?)Flag(after,"nativeCanEngage")),
("nativeRefusalReasonAfter",after is null?null:Number(after,"nativeRefusalReason")),("diagnosticOnly",true)));
while(originalCommands.Count>12)originalCommands.Dequeue();
}
}
catch{/* Keep original behavior even if observation fails. */}
}
[UnmanagedCallersOnly]private static unsafe ulong Observe(nuint sco,float delta,double time)
{
Navigation.RecoveryCruiseBinding? selected;lock(sync){calls++;selected=enabled&&GetCurrentThreadId()==thread&&binding?.component==sco?binding:null;}
if(selected is not null)
{try{simulationStepSeconds=float.IsFinite(delta)&&delta is >0 and <=.25f?delta:double.NaN;exitClock.AcceptIncoming(delta);originalUpdateThirdArgument=double.IsFinite(time)?time:double.NaN;PollControl(selected,sco);}catch(Exception error){lock(sync){errors++;issue=error.GetType().Name+": "+error.Message;try{StopOwned(selected,sco,"control-error");}catch{}}}}
ulong result;originalUpdateDepth++;
try{result=Call(original,(ulong)sco,F32(delta),F64(time));}catch{return 0;}finally{originalUpdateDepth--;}
if(selected is null)return result;
exitClock.Complete(delta);
try
{
var tick=GetTickCount64();if(tick-lastSampleTick<20)return result;lastSampleTick=tick;
JsonElement? target;lock(sync)target=intent?.Target;
var observed=Navigation.ObserveCruiseControl(selected,sco,target,thread,world);if(observed is null)return result;
AttachThermalTiming(observed);
lock(sync)
{
if(selected!=binding)return result;
var at=(ulong)Number(observed,"observedAtUnixMilliseconds");
observed["heatRisePercentPerSecond"]=double.IsFinite(heatSlope)?(double?)heatSlope:null;
observed["heatObservation"]=HeatObservation(at);
observed["overchargeControl"]=true;observed["overchargeCommandsSent"]=enableCalls+disableCalls;latest=observed;matches++;
if(owned&&Number(observed,"state")==2)activeSamples++;
qualification.Observe(entity,world,at,(int)Number(observed,"state"));
if(awaitingOff&&Number(observed,"state") is 0 or 3){offSamples++;awaitingOff=false;}
}
}
catch(Exception error){lock(sync){errors++;issue=error.GetType().Name+": "+error.Message;}}
return result;
}
internal static unsafe uint Start(nuint owningThread)
{
lock(sync)
{
if(started)return thread==owningThread?1u:0u;
if(owningThread is 0 or >uint.MaxValue||ImageHash(ModulePath())!=GameHash)return 0;
image=ImageBase;thread=(uint)owningThread;Navigation.ConfigureRecoveryReadOnlyImage();
if(!ReadBytes(image+Update,10).AsSpan().SequenceEqual(new byte[]{0x48,0x89,0x5C,0x24,0x18,0x57,0x48,0x83,0xEC,0x40}))return 0;
if(HookInitialize() is not(0 or 1))return 0;
var updateHook=HookCreate(image+Update,(nuint)(delegate* unmanaged<nuint,float,double,ulong>)&Observe,out original);
if(updateHook==3)
{
// A disabled 0.5.24 extension can keep its MinHook registration.
// Require its own inactive receipt and the same live stopped
// main ship before removing only that original SCO update hook.
var legacy=NavAsync(Obj(("op","read")),CruiseControlProtocol.LegacyService).GetAwaiter().GetResult();
var raw=Nav(Obj(("op","read")));var at=Utc();
if(legacy.Str("service")!=CruiseControlProtocol.LegacyService||legacy.Int("processId")!=ProcessId||legacy.Str("gameSha256")!=GameHash
||legacy.Bool("enabled")||legacy.Bool("owned")||legacy.U64("leaseExpiresAtUnixMilliseconds")>at
||!raw.Bool("inGame")||!raw.Bool("localPlayerShipVerified")||!Fresh(raw.U64("observedAtUnixMilliseconds"),at))return 0;
var current=Navigation.BindRecoveryCruise(ulong.Parse(raw.Str("entityId")));
if(!Navigation.RecoveryCruiseStopped(current,ulong.Parse(raw.Str("referenceWorldAddress")))||raw.Str("entityId")!=legacy.Str("watchEntityId")
||raw.Str("referenceWorldAddress")!=legacy.Str("watchReferenceWorld")||HookRemove(image+Update)!=0)return 0;
replacedLegacyUpdate=true;
updateHook=HookCreate(image+Update,(nuint)(delegate* unmanaged<nuint,float,double,ulong>)&Observe,out original);
}
if(updateHook!=0)return 0;
var commandHook=HookCreate(image+OriginalSet,(nuint)(delegate* unmanaged<nuint,byte,void>)&ObserveOriginalCommand,out originalSet);
// The verified inactive predecessor retains its pinned, read-only
// command observer. Leave that forwarding hook untouched; only the
// update controller is replaced. Never remove an unknown hook.
if(commandHook==3&&replacedLegacyUpdate)usingLegacyCommandObserver=true;
else if(commandHook!=0)return 0;
if(!Pin((nuint)(delegate* unmanaged<nuint,float,double,ulong>)&Observe)||!Pin((nuint)(delegate* unmanaged<nuint,byte,void>)&ObserveOriginalCommand)
||!usingLegacyCommandObserver&&HookEnable(image+OriginalSet)!=0||HookEnable(image+Update)!=0)return 0;
started=enabled=true;PipeServer.StartJson(Service,32768,Handle);return 1;
}
}
internal static uint SelfTest()
{
if(started||ImageHash(ModulePath())==GameHash)return 0;uint result=0;
var heatTest=new ScoHeatSampleHistory();heatTest.Observe(1000,40);heatTest.Observe(1010,40.2);
heatTest.Observe(1110,41.7);heatTest.Observe(1210,42.7);heatTest.Observe(1270,43.3);
if(Math.Abs(heatTest.ConservativeSlope(1270)-15)>1e-7||heatTest.RecentPeakExpiresAt!=1360
||Math.Abs(heatTest.ConservativeSlope(1361)-10)>1e-7||heatTest.SlopeStartAt!=1210||heatTest.SlopeEndAt!=1270)return 0;
var good=new CruiseOverchargeGateFrame{IdentityVerified=true,GeometryVerified=true,TargetVerified=true,InSupercruise=true,NativeCanEngage=true,
RawFsdState=2,OverchargeState=0,ObservedAt=1000,Hull=1,Heat=40,HeatRise=0,MainFuel=32,FuelCapacity=32,FuelRateUpper=1,RequiredFuelReserve=8,Distance=1e12,Speed=1e6,AimErrorDegrees=0,
ThermalInput=new(){Verified=true,HeatCapacity=100,HeatDisplayScale=1,ScoInputUpper=10,OtherInputUpper=5,
CoolingPassive=10,CoolingMaximum=10,CoolingScale=1,CoolingHeatLower=0,CoolingHeatUpper=1,
LeaseRemainingSeconds=.15,ResponseDelaySeconds=.01,AfterStopSeconds=.5}};
if(Key(new('a',32))&&!Key(new('g',32)))result|=1;
if(CruiseOverchargeNativeGuard.Refusal(good,1100,1250) is null)result|=2;
if(CruiseOverchargeNativeGuard.Refusal(good with{OverchargeState=3},1100,1250) is not null)result|=4;
if(CruiseOverchargeNativeGuard.Refusal(good with{InSupercruise=false},1100,1250) is not null)result|=8;
if(CruiseOverchargeNativeGuard.Refusal(good with{Heat=99.5},1100,1250)=="heat")result|=16;
if(CruiseOverchargeNativeGuard.Refusal(good with{FuelRateUpper=20},1100,1250)=="fuel")result|=32;
if(CruiseOverchargeNativeGuard.Refusal(good,1250,1250)=="expired"&&!Fresh(100,301))result|=64;
var actual=good with{OverchargeState=2,CruiseParametersVerified=true,CruiseApproachSeconds=6,Distance=784.9268650541314*ScoApproachEnvelope.LightSecond,Speed=26.417687132075883*ScoApproachEnvelope.LightSecond};
if(CruiseOverchargeNativeGuard.Refusal(good with{TargetVerified=false},1100,1250) is not null
&&CruiseOverchargeNativeGuard.Refusal(actual,1100,1250) is null
&&CruiseOverchargeNativeGuard.Refusal(actual with{Distance=100*ScoApproachEnvelope.LightSecond},1100,1250)=="approach-or-alignment")result|=128;
foreach(var op in new[]{"activate","input.set","local.select","cruise.set"})
{using var request=JsonDocument.Parse(Obj(("op",op)).ToJsonString());try{Handle(request.RootElement);return result;}catch(InvalidOperationException){}}
result|=256;var status=Describe();if(!status["enabled"]!.GetValue<bool>()&&status["overchargeCommandsSent"]!.GetValue<ulong>()==0)result|=512;
var cycle=new OriginalCruiseCycleQualification();var identity=new OriginalCruiseCycleIdentity(new('a',32),new('b',32),1,2,3);
cycle.Begin(identity,1000,true,1);for(ulong i=1;i<=5;i++){cycle.Observe(2,3,1000+i*20,2);cycle.Observe(2,3,1000+i*20,2);}
if(cycle.ActiveSamples==5&&cycle.Stop(identity,new('c',32),1120,true,2,3,true)&&!cycle.Qualified)
{cycle.Observe(2,3,1140,3);if(cycle.Qualified)result|=1024;}
if(Navigation.TestCruiseMotionHistory())result|=2048;
return result;
}
}
internal static class CruiseControlExports
{
[UnmanagedCallersOnly(EntryPoint="XFEInteractionStart")]internal static uint Start(nuint thread){try{return CruiseControlRuntime.Start(thread);}catch{return 0;}}
[UnmanagedCallersOnly(EntryPoint="XFECruiseControlSelfTest")]internal static uint SelfTest(){try{return CruiseControlRuntime.SelfTest();}catch{return 0;}}
}
#endif
#if PLUGIN_CRUISE_CONTROL
using System.IO.Pipes;
using System.Text;
using XFEEDToolBox.Core.Navigation;
namespace XFEEDToolBox.Plugin;
// The only game mutation is the verified original SCO enable/disable method,
// dispatched from the original main-ship update on its owning thread.
internal static class CruiseControlRuntime
{
private const nuint Update=0x2EADB40,OriginalSet=0x2EAACD0;
private const string Service=CruiseControlProtocol.Service;
private static bool usingLegacyCommandObserver;
private sealed record Intent(string Owner,string RequestId,ulong Generation,ulong SelectionGeneration,ulong Deadline,JsonElement Target,double Reserve);
private static readonly object sync=new();
private static bool started,enabled,owned,releaseRequested,awaitingOff,damageSeen;
private static nuint image,original,originalSet;
[ThreadStatic]private static int ownedSetDepth;
[ThreadStatic]private static int originalUpdateDepth;
private static readonly Queue<JsonObject> originalCommands=new();
private static ulong originalCommandSerial;
private static bool replacedLegacyUpdate;
internal static void EnterOwnedOriginalCommand()=>ownedSetDepth++;
internal static void ExitOwnedOriginalCommand()=>ownedSetDepth--;
private static uint thread;
private static Navigation.RecoveryCruiseBinding? binding;
private static Intent? intent;
private static string activeOwner="";
private static string activeStartRequestId="",lastLeaseRequestId="",releaseRequestId="";
private static ulong activeSelectionGeneration;
private static ulong entity,world,calls,matches,errors,lastSampleTick,generation,consumed,applied,failed,enableCalls,disableCalls,activeSamples,offSamples;
private static readonly ScoHeatSampleHistory heatHistory=new();
private static readonly ScoExitSimulationClock exitClock=new();
private static readonly ScoExitEstimatePolicy exitPolicy=new();
private static double previousHull=double.NaN,previousShield=double.NaN,heatSlope=double.NaN;
private static double simulationStepSeconds=double.NaN;
private static double originalUpdateThirdArgument=double.NaN;
private static ulong lastOriginalStopCallMilliseconds;
private static JsonObject? latest,receipt,lastOriginalStop,lastGate;
private static readonly OriginalCruiseCycleQualification qualification=new();
private static string? issue;
private static bool Key(string value)=>value.Length==32&&value.All(c=>c is >= '0' and <= '9' or >= 'a' and <= 'f');
private static bool Fresh(ulong at,ulong now)=>at<=now&&now-at<=200;
private static void ObserveHeat(ulong at,double heat)
{
heatHistory.Observe(at,heat);heatSlope=heatHistory.Slope;
}
private static double GuardHeatRise(ulong at)=>heatHistory.ConservativeSlope(at);
private static JsonObject HeatObservation(ulong at)=>Obj(("samplingPhase","before-original-update"),("usedForControl",false),("slopePercentPerSecond",heatSlope),
("windowStartUnixMilliseconds",heatHistory.SlopeStartAt),("windowEndUnixMilliseconds",heatHistory.SlopeEndAt),
("windowMilliseconds",heatHistory.SlopeEndAt-heatHistory.SlopeStartAt),("windowStartHeatPercent",heatHistory.SlopeStartHeat),
("windowEndHeatPercent",heatHistory.SlopeEndHeat),("recentPeakPercentPerSecond",heatHistory.RecentPeakRise),("recentPeakExpiresAtUnixMilliseconds",heatHistory.RecentPeakExpiresAt),
("guardHeatRisePercentPerSecond",GuardHeatRise(at)));
private static bool Flag(JsonObject value,string key)=>value[key] is JsonValue v&&v.TryGetValue<bool>(out var flag)&&flag;
private static double Number(JsonObject value,string key)
{
if(value[key] is not JsonValue v)return double.NaN;
if(v.TryGetValue<double>(out var d))return d;if(v.TryGetValue<float>(out var f))return f;
if(v.TryGetValue<int>(out var i))return i;if(v.TryGetValue<uint>(out var u))return u;
if(v.TryGetValue<byte>(out var b))return b;if(v.TryGetValue<ulong>(out var n))return n;
return double.NaN;
}
private static void AttachThermalTiming(JsonObject sample)
{
sample["originalUpdateClockEpoch"]=exitClock.Epoch;sample["originalUpdateCompletedSteps"]=exitClock.CompletedSteps;
sample["originalUpdateCompletedSimulationSeconds"]=exitClock.Valid?(double?)exitClock.CompletedSeconds:null;
sample["originalUpdateClockValid"]=exitClock.Valid;
sample["originalUpdateClockScope"]="Sum of every completed matched original SCO callback delta; no sparse IPC dt or UTC substitution";
sample["scoExitApproach"]=exitPolicy.Latest.Json();
sample["sameFrameSimulationStepSeconds"]=double.IsFinite(simulationStepSeconds)?(double?)simulationStepSeconds:null;
sample["sameFrameSimulationStepSource"]="owning original SCO update delta argument";
sample["originalUpdateThirdArgument"]=double.IsFinite(originalUpdateThirdArgument)?(double?)originalUpdateThirdArgument:null;
sample["originalUpdateThirdArgumentMeaningVerified"]=false;
if(sample["thermal"] is not JsonObject thermal)return;
var heatStep=Number(thermal,"nativeHeatStepSeconds");
var response=Math.Max(simulationStepSeconds,heatStep);
thermal["responseDelaySeconds"]=double.IsFinite(response)&&response is >0 and <=.25?(double?)response:null;
thermal["afterStopObservationSeconds"]=.5;
thermal["heatNumericalUncertaintyPercent"]=.001;
thermal["stopCallWallMilliseconds"]=lastOriginalStopCallMilliseconds;
thermal["currentSimulationStepSeconds"]=double.IsFinite(simulationStepSeconds)?(double?)simulationStepSeconds:null;
thermal["wallLeaseIsSimulationBound"]=false;
thermal["timingBasis"]="Current actual simulation dt is guarded before original Active heat input; UTC lease is not a proven cumulative simulation horizon; synchronous stop wall time does not continue SCO heating";
}
private static ScoThermalInput? ThermalInput(JsonObject sample,ulong now,ulong deadline)
{
if(sample["thermal"] is not JsonObject thermal)return null;
return new(){Verified=Flag(thermal,"verified")&&thermal["entityId"]?.ToString()==entity.ToString()
&&thermal["referenceWorldAddress"]?.ToString()==world.ToString(),
HeatPercent=Number(sample,"heatPercent"),HeatCapacity=Number(thermal,"heatCapacity"),HeatDisplayScale=Number(thermal,"heatDisplayScale"),
ScoInputUpper=Number(thermal,"scoInputUpper"),OtherInputUpper=Number(thermal,"otherInputUpper"),
CoolingPassive=Number(thermal,"coolingPassive"),CoolingMaximum=Number(thermal,"coolingMaximum"),CoolingScale=Number(thermal,"coolingScale"),
CoolingHeatLower=Number(thermal,"coolingHeatLower"),CoolingHeatUpper=Number(thermal,"coolingHeatUpper"),
SourceAgeSeconds=(now-Number(thermal,"sourceObservedAtUnixMilliseconds"))/1000d,
HeatAgeSeconds=(now-Number(thermal,"heatObservedAtUnixMilliseconds"))/1000d,
LeaseRemainingSeconds=deadline>now?(deadline-now)/1000d:0,ResponseDelaySeconds=Number(thermal,"responseDelaySeconds"),
AfterStopSeconds=.5,HeatUncertaintyPercent=.001};
}
private static bool CurrentSimulationHeatBound(JsonObject sample,ScoThermalInput? model,ulong now,out double peak)
{
peak=double.NaN;if(model is null||sample["thermal"] is not JsonObject thermal)return false;
var storedHeat=Number(thermal,"currentStoredEnergyHeatPercent");var storedAt=Number(thermal,"currentStoredEnergyObservedAtUnixMilliseconds");
if(!double.IsFinite(storedHeat)||storedHeat<0||!double.IsFinite(storedAt)||storedAt>now||now-storedAt>200)return false;
var heat=Math.Max(Number(sample,"heatPercent"),storedHeat);
return ScoThermalPredictor.TryBoundCurrentSimulationStep(model with{HeatPercent=heat,HeatAgeSeconds=0},simulationStepSeconds,out peak);
}
private static JsonObject? ThermalForecast(ScoThermalForecast? forecast)=>forecast is null?null:Obj(("known",forecast.Known),
("scoOnNetRisePercentPerSecond",double.IsFinite(forecast.ScoOnNetRisePercentPerSecond)?(double?)forecast.ScoOnNetRisePercentPerSecond:null),
("scoOffNetRisePercentPerSecond",double.IsFinite(forecast.ScoOffNetRisePercentPerSecond)?(double?)forecast.ScoOffNetRisePercentPerSecond:null),
("secondsTo100LowerBound",forecast.SecondsTo100LowerBound),
("continueUntilDeadlinePeakPercent",double.IsFinite(forecast.ContinueUntilDeadlinePeakPercent)?(double?)forecast.ContinueUntilDeadlinePeakPercent:null),
("deadlineStopPeakPercent",double.IsFinite(forecast.DeadlineStopPeakPercent)?(double?)forecast.DeadlineStopPeakPercent:null),
("immediateStopPeakPercent",double.IsFinite(forecast.ImmediateStopPeakPercent)?(double?)forecast.ImmediateStopPeakPercent:null),
("immediateStopEndPercent",double.IsFinite(forecast.ImmediateStopEndPercent)?(double?)forecast.ImmediateStopEndPercent:null),
("deadlineStopEndPercent",double.IsFinite(forecast.DeadlineStopEndPercent)?(double?)forecast.DeadlineStopEndPercent:null),
("stopHorizonSeconds",forecast.StopHorizonSeconds),("shouldStop",forecast.ShouldStop),("reason",forecast.Reason));
private static async Task<JsonElement> NavAsync(JsonObject request,string service="Navigation")
{
using var timeout=new CancellationTokenSource(TimeSpan.FromMilliseconds(150));
using var pipe=new NamedPipeClientStream(".",$"XFEEDToolBox.{service}.{ProcessId}",PipeDirection.InOut,PipeOptions.Asynchronous);
await pipe.ConnectAsync(timeout.Token);await pipe.WriteAsync(Encoding.UTF8.GetBytes(request.ToJsonString()+"\n"),timeout.Token);await pipe.FlushAsync(timeout.Token);
var bytes=new byte[65536];var count=0;
while(count<bytes.Length){var n=await pipe.ReadAsync(bytes.AsMemory(count),timeout.Token);if(n==0)break;count+=n;if(bytes[count-1]=='\n')break;}
if(count is 0 or 65536)throw new IOException("Invalid original navigation response.");
using var document=JsonDocument.Parse(bytes.AsMemory(0,count));var response=document.RootElement;
if(!response.GetProperty("ok").GetBoolean())throw new InvalidOperationException(response.Str("error"));
return response.GetProperty("result").Clone();
}
private static JsonElement Nav(JsonObject request)=>NavAsync(request).GetAwaiter().GetResult();
private static bool SameTarget(JsonElement a,JsonElement b)=>a.Int("key")==b.Int("key")&&a.Str("bodyAddress")==b.Str("bodyAddress")
&&a.U64("kind")==b.U64("kind")&&a.U64("marketId")==b.U64("marketId")&&a.Str("name")==b.Str("name");
private static JsonObject Describe()
{
lock(sync)
{
var observed=latest is null?0:(ulong)Number(latest,"observedAtUnixMilliseconds");
return Obj(("schema",1),("service",Service),("version",Platform.PluginVersion),("processId",ProcessId),("gameSha256",GameHash),("enabled",enabled),("owningThread",thread),
("replacedIdleLegacyUpdate",replacedLegacyUpdate),("usingReadOnlyLegacyCommandObserver",usingLegacyCommandObserver),
("watchEntityId",entity.ToString()),("watchReferenceWorld",world.ToString()),("watchInterface",binding?.component.ToString("x")),
("sampleReady",observed!=0&&Fresh(observed,Utc())),("sample",latest?.DeepClone()),("overchargeControl",true),
("originalFuelRangeObservations",true),("componentThermalModel",true),("nativeStopReceipts",true),("expiringOriginalControl",true),("controlTelemetrySchema",1),
("overchargeControlQualified",qualification.Qualified),("cycleActiveSamples",qualification.ActiveSamples),("qualifiedCycle",QualifiedCycle()),
("owned",owned),("leaseExpiresAtUnixMilliseconds",intent?.Deadline??0),("receipt",receipt?.DeepClone()),("lastOriginalStop",lastOriginalStop?.DeepClone()),("lastGate",lastGate?.DeepClone()),
("scoExitEstimate",exitPolicy.Status()),
("originalCommandObservations",new JsonArray(originalCommands.Select(value=>(JsonNode)value.DeepClone()).ToArray())),("originalCommandSerial",originalCommandSerial),
("generation",generation),("consumed",consumed),("applied",applied),("failed",failed),
("calls",calls),("matches",matches),("errors",errors),("issue",issue),("originalEnableCalls",enableCalls),("originalDisableCalls",disableCalls),
("actualActiveSamples",activeSamples),("actualOffSamples",offSamples),("overchargeCommandsSent",enableCalls+disableCalls),
("steeringInputsSent",0),("targetSelectionsSent",0));
}
}
private static JsonObject? QualifiedCycle()
{
var proof=qualification.Completed;if(proof is null)return null;var identity=proof.Identity;
return Obj(("owner",identity.Owner),("startRequestId",identity.StartRequestId),("selectionGeneration",identity.SelectionGeneration),
("entityId",identity.Entity.ToString()),("referenceWorldAddress",identity.World.ToString()),("activeSamples",proof.ActiveSamples),
("enableAtUnixMilliseconds",proof.EnableAt),("lastActiveAtUnixMilliseconds",proof.LastActiveAt),("releaseRequestId",proof.StopRequestId),
("originalStopAtUnixMilliseconds",proof.StopAt),("offAtUnixMilliseconds",proof.OffAt));
}
private static JsonNode Handle(JsonElement request)
{
var op=request.Str("op");if(op is "describe" or "read")return Describe();
if(op=="sco.exit.configure")
{
lock(sync)
{
if(request.Str("mode")=="shadow"){exitPolicy.ShadowOnly();return Describe();}
var mode=request.Str("mode");
if(mode is not("trial" or "production")||owned||intent is not null)throw new InvalidOperationException("Exit strategy configuration requires an idle owned-command channel.");
var expectedEntity=request.U64("entityId");var expectedWorld=request.U64("referenceWorldAddress");
string why;
var accepted=mode=="production"
?exitPolicy.PromoteProduction(expectedEntity,expectedWorld,Utc(),request.U64("expiresAtUnixMilliseconds"),out why)
:exitPolicy.ArmTrial(expectedEntity,expectedWorld,Utc(),request.U64("expiresAtUnixMilliseconds"),request.Int("maximumBursts"),out why);
if(!accepted)throw new InvalidOperationException("SCO exit strategy refused: "+why);
return Describe();
}
}
if(op=="deactivate")
{
lock(sync){if(owned||releaseRequested)throw new InvalidOperationException("Release the owned SCO and observe its original stopped state first.");enabled=false;}
HookDisable(image+Update);return Describe();
}
if(!enabled)throw new InvalidOperationException("Original cruise controller is inactive.");
if(op=="watch")
{
var fingerprint=request.Str("loadoutFingerprint");
if(fingerprint.Length!=0&&(fingerprint.Length!=64||!fingerprint.All(Uri.IsHexDigit)))throw new InvalidOperationException("Trusted SHA256 loadout fingerprint required.");
lock(sync)if(owned)throw new InvalidOperationException("Cannot rebind an owned SCO lease.");
var frame=Nav(Obj(("op","read")));var health=frame.GetProperty("shipHealth");var now=Utc();
if(!frame.Bool("inGame")||!frame.Bool("localPlayerShipVerified")||!health.Bool("verified")||!Fresh(frame.U64("observedAtUnixMilliseconds"),now)
||frame.Str("entityId")!=health.Str("entityId"))throw new InvalidOperationException("Fresh original main ship required.");
var next=ulong.Parse(frame.Str("entityId"));var reference=ulong.Parse(frame.Str("referenceWorldAddress"));var selected=Navigation.BindRecoveryCruise(next);
lock(sync)
{
if(owned)throw new InvalidOperationException("Cannot rebind an owned SCO lease.");
if(entity!=next||world!=reference||binding?.component!=selected.component)
{latest=lastGate=null;heatHistory.Reset();exitClock.Reset();exitPolicy.ResetBinding();previousHull=previousShield=heatSlope=double.NaN;damageSeen=false;intent=null;releaseRequested=awaitingOff=false;qualification.ForgetCurrent();}
exitPolicy.BindLoadout(fingerprint);
entity=next;world=reference;binding=selected;issue=selected.component==0?"current-entity-has-no-SCO-interface":"awaiting-original-main-ship-update";
}
return Describe();
}
if(op=="cruise.release")
{
var owner=request.Str("owner");var stopId=request.Str("requestId");if(!Key(owner)||!Key(stopId))throw new InvalidOperationException("Explicit SCO owner and stop request required.");
lock(sync)
{
if(owned&&activeOwner!=owner||intent is not null&&intent.Owner!=owner)throw new InvalidOperationException("SCO belongs to another controller.");
if(owned){releaseRequestId=stopId;releaseRequested=true;}consumed=generation;intent=null;
return Obj(("requestId",stopId),("released",!owned),("pending",owned),("originalDisableCalls",disableCalls),("lastOriginalStop",lastOriginalStop?.DeepClone()));
}
}
if(op!="cruise.set")throw new InvalidOperationException("Unknown original cruise command.");
var ownerId=request.Str("owner");var requestId=request.Str("requestId");var deadline=request.U64("expiresAtUnixMilliseconds");var sequence=request.U64("sequence");
var reserve=request.Num("requiredFuelReserveTonnes",double.NaN);var selection=request.U64("selectionGeneration");var began=Utc();
if(!Key(ownerId)||!Key(requestId)||selection==0||sequence==0||deadline<=began||deadline-began>250||!double.IsFinite(reserve)||reserve<=0)
throw new InvalidOperationException("A bounded original SCO request is required.");
lock(sync)
{
if(intent?.RequestId==requestId)return Describe();
if(owned&&activeOwner!=ownerId||intent is not null&&intent.Owner!=ownerId&&(owned||intent.Deadline>began))throw new InvalidOperationException("Another controller owns SCO.");
if(owned&&activeSelectionGeneration!=selection)throw new InvalidOperationException("Release SCO before changing its original target selection.");
if(intent is not null&&consumed<intent.Generation&&intent.Deadline>began)throw new InvalidOperationException("Previous SCO request is still pending.");
if(!owned&&latest is not null&&Number(latest,"state") is 1 or 2)throw new InvalidOperationException("SCO was already active outside this controller.");
}
var raw=Nav(Obj(("op","read")));var local=Nav(Obj(("op","local.read")));
var status=Nav(Obj(("op","local.status"),("owner",ownerId),("generation",selection)));var at=Utc();
var healthNow=raw.GetProperty("shipHealth");var referenceTarget=request.Required("target");
if(!raw.Bool("inGame")||!raw.Bool("localPlayerShipVerified")||raw.Bool("playerOverride")||!healthNow.Bool("verified")||healthNow.Bool("emergency")
||!Fresh(raw.U64("observedAtUnixMilliseconds"),at)||!Fresh(local.U64("observedAtUnixMilliseconds"),at)||deadline<=at||deadline>local.U64("observedAtUnixMilliseconds")+250
||!status.Bool("selected")||!status.Bool("telemetryReady")||!local.Bool("inSupercruise")||local.Int("rawFsdState")!=2
||!SameTarget(referenceTarget,local.Required("target"))||!SameTarget(referenceTarget,status.Required("target"))
||sequence>local.U64("sequence")||local.U64("sequence")-sequence>16||raw.Str("entityId")!=local.Str("entityId")
||raw.Str("referenceWorldAddress")!=local.Str("referenceWorldAddress"))throw new InvalidOperationException("Original target owner or fresh SC reference changed.");
lock(sync)
{
if(!enabled||binding?.component is null or 0||raw.Str("entityId")!=entity.ToString()||raw.Str("referenceWorldAddress")!=world.ToString()
||owned&&activeOwner!=ownerId||intent is not null&&intent.Owner!=ownerId&&owned||deadline<=Utc())throw new InvalidOperationException("Current main-ship SCO binding changed.");
intent=new(ownerId,requestId,++generation,selection,deadline,local.Required("target").Clone(),reserve);releaseRequested=false;
receipt=Obj(("owner",ownerId),("requestId",requestId),("generation",generation),("selectionGeneration",selection),("queued",true),("applied",false),("nativeMethodCalled",false));return Describe();
}
}
private static void StopOwned(Navigation.RecoveryCruiseBinding selected,nuint sco,string reason,JsonObject? telemetryFailure=null,JsonObject? gate=null)
{
if(!owned)return;
var callStarted=Utc();
if(!Navigation.SetOriginalCruise(selected,sco,false,out var state,out var called,out var before)){issue="original-disable-not-confirmed:"+reason;return;}
lastOriginalStopCallMilliseconds=Utc()-callStarted;
exitPolicy.OriginalStopped(called,before,state);
var stoppedAt=Utc();qualification.Stop(new(activeOwner,activeStartRequestId,activeSelectionGeneration,entity,world),releaseRequestId,stoppedAt,called,before,state,reason=="released");
lastOriginalStop=Obj(("owner",activeOwner),("startRequestId",activeStartRequestId),("leaseRequestId",lastLeaseRequestId),
("selectionGeneration",activeSelectionGeneration),("releaseRequestId",releaseRequestId.Length==0?null:releaseRequestId),("entityId",entity.ToString()),("referenceWorldAddress",world.ToString()),
("nativeMethodRva",called?"2eaacd0":null),("nativeMethodCalled",called),("argument",false),("stateBefore",before),("stateAfter",state),
("reason",reason),("telemetryFailure",telemetryFailure?.DeepClone()),("gate",gate?.DeepClone()),("stopCallWallMilliseconds",lastOriginalStopCallMilliseconds),("observedAtUnixMilliseconds",stoppedAt),("owningThread",GetCurrentThreadId()));
if(called)disableCalls++;owned=false;activeOwner="";releaseRequested=false;awaitingOff=true;issue=reason;
}
private static void PollControl(Navigation.RecoveryCruiseBinding selected,nuint sco)
{
lock(sync)
{
var wanted=intent;var now=Utc();
if(releaseRequested||wanted is null||wanted.Deadline<=now)
{StopOwned(selected,sco,releaseRequested?"released":wanted is null?"no-lease":"lease-expired");wanted=null;}
// Derive heat only here, once per original update. Mixing before/after
// readings divides a whole simulation heat step by a partial wall-clock
// interval and can retain that artificial peak for the next 250 ms.
// Continue sampling without a lease so starting requires a real baseline.
var sample=Navigation.ObserveCruiseControl(selected,sco,wanted?.Target,thread,world);
if(sample is not null)
{
exitPolicy.Observe(ScoExitNativeFrame.Read(sample,exitClock.Epoch,exitClock.CompletedSteps,exitClock.CompletedSeconds,exitClock.Valid,simulationStepSeconds));
AttachThermalTiming(sample);ObserveHeat((ulong)Number(sample,"observedAtUnixMilliseconds"),Number(sample,"heatPercent"));
}
else{heatHistory.Reset();heatSlope=double.NaN;}
if(wanted is null)return;
string? reason="native-telemetry";
JsonObject? gateObservation=null;
if(sample is not null)
{
// Sampling stamps the actual frame. Evaluate its age only after
// that sampling has finished; the pre-sample clock is older.
now=Utc();
var at=(ulong)Number(sample,"observedAtUnixMilliseconds");var heat=Number(sample,"heatPercent");
var hull=Number(sample,"hullFraction");var shield=Number(sample,"shieldFraction");
if(double.IsFinite(previousHull)&&hull<previousHull-1e-6||double.IsFinite(previousShield)&&shield<previousShield-1e-6)damageSeen=true;
previousHull=hull;previousShield=shield;
var state=(int)Number(sample,"state");
var gate=new CruiseOverchargeGateFrame{IdentityVerified=Flag(sample,"verified"),GeometryVerified=Flag(sample,"geometryVerified"),
TargetVerified=Flag(sample,"targetVerified"),InSupercruise=Flag(sample,"inSupercruise"),RawFsdState=(int)Number(sample,"rawFsdState"),
AssistEnabled=Flag(sample,"nativeAssistEnabled"),CollisionCourse=Flag(sample,"collisionCourse"),OverchargeState=state,
Emergency=Flag(sample,"healthEmergency")||Flag(sample,"playerOverride")||damageSeen&&double.IsFinite(shield)&&shield<=1e-6,
NativeCanEngage=Flag(sample,"nativeCanEngage"),ObservedAt=at,Hull=hull,Heat=heat,
HeatRise=GuardHeatRise(at),ThermalInput=ThermalInput(sample,now,wanted.Deadline),
MainFuel=Number(sample,"fuelMainTonnes"),FuelCapacity=Number(sample,"fuelCapacityTonnes"),FuelRateUpper=Flag(sample,"fuelBoundVerified")?Number(sample,"fuelRateUpperTonnesPerSecond"):double.NaN,
RequiredFuelReserve=wanted.Reserve,Distance=Number(sample,"distanceMetres"),Speed=Number(sample,"speedMetresPerSecond"),AimErrorDegrees=Number(sample,"aimErrorDegrees"),
CruiseParametersVerified=Flag(sample,"cruiseParametersVerified"),CruiseApproachSeconds=Number(sample,"cruiseApproachSeconds")};
gate=gate with{ShipEntity=entity,ReferenceWorld=world,TargetKey=wanted.Target.Int("key"),ExitApproach=exitPolicy.Latest};
reason=CruiseOverchargeNativeGuard.Refusal(gate,now,wanted.Deadline);
// This guard covers the exact incoming simulation step, independent of UTC-to-simulation clock mapping.
// Do not spend cooling from a later HeatTick before this Active call has submitted its gross heat input.
var frameHeatKnown=CurrentSimulationHeatBound(sample,gate.ThermalInput,now,out var frameHeatPeak);
if(reason is null&&!frameHeatKnown)reason="thermal-model";
if(reason is null&&frameHeatPeak>=100)reason="heat";
var thermalForecast=gate.ThermalInput is {} thermal?ScoThermalPredictor.Predict(thermal):null;
gateObservation=Obj(("sampleObservedAtUnixMilliseconds",at),("evaluatedAtUnixMilliseconds",now),("leaseExpiresAtUnixMilliseconds",wanted.Deadline),
("heatPercent",gate.Heat),("heatRisePercentPerSecond",gate.HeatRise),
("predictedHeatPercent",thermalForecast is {Known:true}?thermalForecast.DeadlineStopPeakPercent:(double?)null),
("ceilingHeatPercent",100),("thermalModel",sample["thermal"]?.DeepClone()),("thermalForecast",ThermalForecast(thermalForecast)),
("currentFrameGrossHeatBoundKnown",frameHeatKnown),("currentFrameGrossHeatPeakPercent",frameHeatKnown?(double?)frameHeatPeak:null),
("currentSimulationStepSeconds",double.IsFinite(simulationStepSeconds)?(double?)simulationStepSeconds:null),
("currentFrameHeatBoundScope","Full-pressure SCO input and recent other-source envelope over this actual simulation dt; no future radiator credit; other future sources are not an absolute bound"),
("identityVerified",gate.IdentityVerified),("targetVerified",gate.TargetVerified),("geometryVerified",gate.GeometryVerified),
("inSupercruise",gate.InSupercruise),("rawFsdState",gate.RawFsdState),("nativeCanEngage",gate.NativeCanEngage),("nativeRefusalReason",Number(sample!,"nativeRefusalReason")),
("assistEnabled",gate.AssistEnabled),("collisionCourse",gate.CollisionCourse),("emergency",gate.Emergency),
("overchargeState",state),("hullFraction",hull),("shieldFraction",shield),("damageSeen",damageSeen),
("distanceMetres",gate.Distance),("speedMetresPerSecond",gate.Speed),("aimErrorDegrees",gate.AimErrorDegrees),
("cruiseParametersVerified",gate.CruiseParametersVerified),("cruiseApproachSeconds",double.IsFinite(gate.CruiseApproachSeconds)?(double?)gate.CruiseApproachSeconds:null),
("approachStopHorizonSeconds",ScoApproachEnvelope.StopHorizonSeconds(gate.CruiseParametersVerified,gate.CruiseApproachSeconds)),
("scoExitApproach",exitPolicy.Latest.Json()),
("mainFuelTonnes",gate.MainFuel),("fuelCapacityTonnes",gate.FuelCapacity),("requiredFuelReserveTonnes",gate.RequiredFuelReserve),
("fuelRateUpperTonnesPerSecond",gate.FuelRateUpper),("heatObservation",HeatObservation(at)));
lastGate=gateObservation;
if(!owned&&state is 1 or 2)reason="foreign-active";
}
if(reason is not null)
{
StopOwned(selected,sco,reason,sample is null?Navigation.CruiseTelemetryFailure():null,gateObservation);issue="interlock:"+reason;
if(consumed<wanted.Generation)
{
consumed=failed=wanted.Generation;receipt=Obj(("owner",wanted.Owner),("requestId",wanted.RequestId),("generation",wanted.Generation),
("selectionGeneration",wanted.SelectionGeneration),("applied",false),("failed",true),("reason",reason),("nativeMethodCalled",false),
("nativeObservedAtUnixMilliseconds",sample is null?null:(ulong?)Number(sample,"observedAtUnixMilliseconds")),
("evaluatedAtUnixMilliseconds",now),("expiresAtUnixMilliseconds",wanted.Deadline),("gate",gateObservation?.DeepClone()));
}
return;
}
if(consumed>=wanted.Generation)return;
if(wanted.Deadline<=Utc())
{consumed=failed=wanted.Generation;StopOwned(selected,sco,"expired-before-original-call");receipt=Obj(("requestId",wanted.RequestId),("generation",wanted.Generation),("applied",false),("failed",true),("reason","expired-before-original-call"),("nativeMethodCalled",false));return;}
var called=false;var actualState=(byte)Number(sample!,"state");
if(!owned)
{
if(!Navigation.SetOriginalCruise(selected,sco,true,out actualState,out called,out var before)){consumed=failed=wanted.Generation;issue="original-enable-not-confirmed";return;}
if(called)enableCalls++;owned=true;activeOwner=wanted.Owner;activeSelectionGeneration=wanted.SelectionGeneration;activeStartRequestId=wanted.RequestId;releaseRequestId="";
exitPolicy.BeginOwnedBurst(called,before,actualState);
qualification.Begin(new(activeOwner,activeStartRequestId,activeSelectionGeneration,entity,world),Utc(),called,actualState);
}
lastLeaseRequestId=wanted.RequestId;
consumed=applied=wanted.Generation;receipt=Obj(("owner",wanted.Owner),("requestId",wanted.RequestId),("generation",wanted.Generation),("selectionGeneration",wanted.SelectionGeneration),
("entityId",entity.ToString()),("referenceWorldAddress",world.ToString()),("queued",false),("applied",true),
("nativeMethodCalled",called),("nativeMethodRva",called?"2eaacd0":null),("argument",called?(bool?)true:null),("actualStateAfterCall",actualState),
("observedAtUnixMilliseconds",Utc()),("owningThread",GetCurrentThreadId()));issue=null;
}
}
// Observe all actual original commands, including engine-side cancellation.
// The original method is always forwarded exactly once; this hook decides nothing.
[UnmanagedCallersOnly]private static unsafe void ObserveOriginalCommand(nuint sco,byte engage)
{
Navigation.RecoveryCruiseBinding? selected=null;JsonObject? before=null;string source="game-other";ulong began=0;
try
{
lock(sync)
{
if(enabled&&GetCurrentThreadId()==thread&&binding?.component==sco)
{selected=binding;source=ownedSetDepth>0?"toolbox-original-command":originalUpdateDepth>0?"original-sco-update":"game-other";began=Utc();}
}
if(selected is not null)before=Navigation.ObserveRecoveryCruise(selected,sco);
}
catch{/* Diagnostics cannot prevent forwarding the original call. */}
try{Call(originalSet,sco,engage);}
catch{return;}
if(selected is null)return;
try
{
var after=Navigation.ObserveRecoveryCruise(selected,sco);
lock(sync)
{
originalCommands.Enqueue(Obj(("serial",++originalCommandSerial),("source",source),("argument",engage!=0),
("owner",owned?activeOwner:intent?.Owner),("startRequestId",activeStartRequestId),("leaseRequestId",intent?.RequestId),
("selectionGeneration",intent?.SelectionGeneration??activeSelectionGeneration),("entityId",entity.ToString()),("referenceWorldAddress",world.ToString()),
("observedAtUnixMilliseconds",began),("returnedAtUnixMilliseconds",Utc()),("owningThread",GetCurrentThreadId()),
("stateBefore",before is null?null:Number(before,"state")),("stateAfter",after is null?null:Number(after,"state")),
("nativeCanEngageBefore",before is null?null:(bool?)Flag(before,"nativeCanEngage")),
("nativeRefusalReasonBefore",before is null?null:Number(before,"nativeRefusalReason")),
("nativeCanEngageAfter",after is null?null:(bool?)Flag(after,"nativeCanEngage")),
("nativeRefusalReasonAfter",after is null?null:Number(after,"nativeRefusalReason")),("diagnosticOnly",true)));
while(originalCommands.Count>12)originalCommands.Dequeue();
}
}
catch{/* Keep original behavior even if observation fails. */}
}
[UnmanagedCallersOnly]private static unsafe ulong Observe(nuint sco,float delta,double time)
{
Navigation.RecoveryCruiseBinding? selected;lock(sync){calls++;selected=enabled&&GetCurrentThreadId()==thread&&binding?.component==sco?binding:null;}
if(selected is not null)
{try{simulationStepSeconds=float.IsFinite(delta)&&delta is >0 and <=.25f?delta:double.NaN;exitClock.AcceptIncoming(delta);originalUpdateThirdArgument=double.IsFinite(time)?time:double.NaN;PollControl(selected,sco);}catch(Exception error){lock(sync){errors++;issue=error.GetType().Name+": "+error.Message;try{StopOwned(selected,sco,"control-error");}catch{}}}}
ulong result;originalUpdateDepth++;
try{result=Call(original,(ulong)sco,F32(delta),F64(time));}catch{return 0;}finally{originalUpdateDepth--;}
if(selected is null)return result;
exitClock.Complete(delta);
try
{
var tick=GetTickCount64();if(tick-lastSampleTick<20)return result;lastSampleTick=tick;
JsonElement? target;lock(sync)target=intent?.Target;
var observed=Navigation.ObserveCruiseControl(selected,sco,target,thread,world);if(observed is null)return result;
AttachThermalTiming(observed);
lock(sync)
{
if(selected!=binding)return result;
var at=(ulong)Number(observed,"observedAtUnixMilliseconds");
observed["heatRisePercentPerSecond"]=double.IsFinite(heatSlope)?(double?)heatSlope:null;
observed["heatObservation"]=HeatObservation(at);
observed["overchargeControl"]=true;observed["overchargeCommandsSent"]=enableCalls+disableCalls;latest=observed;matches++;
if(owned&&Number(observed,"state")==2)activeSamples++;
qualification.Observe(entity,world,at,(int)Number(observed,"state"));
if(awaitingOff&&Number(observed,"state") is 0 or 3){offSamples++;awaitingOff=false;}
}
}
catch(Exception error){lock(sync){errors++;issue=error.GetType().Name+": "+error.Message;}}
return result;
}
internal static unsafe uint Start(nuint owningThread)
{
lock(sync)
{
if(started)return thread==owningThread?1u:0u;
if(owningThread is 0 or >uint.MaxValue||ImageHash(ModulePath())!=GameHash)return 0;
image=ImageBase;thread=(uint)owningThread;Navigation.ConfigureRecoveryReadOnlyImage();
if(!ReadBytes(image+Update,10).AsSpan().SequenceEqual(new byte[]{0x48,0x89,0x5C,0x24,0x18,0x57,0x48,0x83,0xEC,0x40}))return 0;
if(HookInitialize() is not(0 or 1))return 0;
var updateHook=HookCreate(image+Update,(nuint)(delegate* unmanaged<nuint,float,double,ulong>)&Observe,out original);
if(updateHook==3)
{
// A disabled 0.5.24 extension can keep its MinHook registration.
// Require its own inactive receipt and the same live stopped
// main ship before removing only that original SCO update hook.
var legacy=NavAsync(Obj(("op","read")),CruiseControlProtocol.LegacyService).GetAwaiter().GetResult();
var raw=Nav(Obj(("op","read")));var at=Utc();
if(legacy.Str("service")!=CruiseControlProtocol.LegacyService||legacy.Int("processId")!=ProcessId||legacy.Str("gameSha256")!=GameHash
||legacy.Bool("enabled")||legacy.Bool("owned")||legacy.U64("leaseExpiresAtUnixMilliseconds")>at
||!raw.Bool("inGame")||!raw.Bool("localPlayerShipVerified")||!Fresh(raw.U64("observedAtUnixMilliseconds"),at))return 0;
var current=Navigation.BindRecoveryCruise(ulong.Parse(raw.Str("entityId")));
if(!Navigation.RecoveryCruiseStopped(current,ulong.Parse(raw.Str("referenceWorldAddress")))||raw.Str("entityId")!=legacy.Str("watchEntityId")
||raw.Str("referenceWorldAddress")!=legacy.Str("watchReferenceWorld")||HookRemove(image+Update)!=0)return 0;
replacedLegacyUpdate=true;
updateHook=HookCreate(image+Update,(nuint)(delegate* unmanaged<nuint,float,double,ulong>)&Observe,out original);
}
if(updateHook!=0)return 0;
var commandHook=HookCreate(image+OriginalSet,(nuint)(delegate* unmanaged<nuint,byte,void>)&ObserveOriginalCommand,out originalSet);
// The verified inactive predecessor retains its pinned, read-only
// command observer. Leave that forwarding hook untouched; only the
// update controller is replaced. Never remove an unknown hook.
if(commandHook==3&&replacedLegacyUpdate)usingLegacyCommandObserver=true;
else if(commandHook!=0)return 0;
if(!Pin((nuint)(delegate* unmanaged<nuint,float,double,ulong>)&Observe)||!Pin((nuint)(delegate* unmanaged<nuint,byte,void>)&ObserveOriginalCommand)
||!usingLegacyCommandObserver&&HookEnable(image+OriginalSet)!=0||HookEnable(image+Update)!=0)return 0;
started=enabled=true;PipeServer.StartJson(Service,32768,Handle);return 1;
}
}
internal static uint SelfTest()
{
if(started||ImageHash(ModulePath())==GameHash)return 0;uint result=0;
var heatTest=new ScoHeatSampleHistory();heatTest.Observe(1000,40);heatTest.Observe(1010,40.2);
heatTest.Observe(1110,41.7);heatTest.Observe(1210,42.7);heatTest.Observe(1270,43.3);
if(Math.Abs(heatTest.ConservativeSlope(1270)-15)>1e-7||heatTest.RecentPeakExpiresAt!=1360
||Math.Abs(heatTest.ConservativeSlope(1361)-10)>1e-7||heatTest.SlopeStartAt!=1210||heatTest.SlopeEndAt!=1270)return 0;
var good=new CruiseOverchargeGateFrame{IdentityVerified=true,GeometryVerified=true,TargetVerified=true,InSupercruise=true,NativeCanEngage=true,
RawFsdState=2,OverchargeState=0,ObservedAt=1000,Hull=1,Heat=40,HeatRise=0,MainFuel=32,FuelCapacity=32,FuelRateUpper=1,RequiredFuelReserve=8,Distance=1e12,Speed=1e6,AimErrorDegrees=0,
ThermalInput=new(){Verified=true,HeatCapacity=100,HeatDisplayScale=1,ScoInputUpper=10,OtherInputUpper=5,
CoolingPassive=10,CoolingMaximum=10,CoolingScale=1,CoolingHeatLower=0,CoolingHeatUpper=1,
LeaseRemainingSeconds=.15,ResponseDelaySeconds=.01,AfterStopSeconds=.5}};
if(Key(new('a',32))&&!Key(new('g',32)))result|=1;
if(CruiseOverchargeNativeGuard.Refusal(good,1100,1250) is null)result|=2;
if(CruiseOverchargeNativeGuard.Refusal(good with{OverchargeState=3},1100,1250) is not null)result|=4;
if(CruiseOverchargeNativeGuard.Refusal(good with{InSupercruise=false},1100,1250) is not null)result|=8;
if(CruiseOverchargeNativeGuard.Refusal(good with{Heat=99.5},1100,1250)=="heat")result|=16;
if(CruiseOverchargeNativeGuard.Refusal(good with{FuelRateUpper=20},1100,1250)=="fuel")result|=32;
if(CruiseOverchargeNativeGuard.Refusal(good,1250,1250)=="expired"&&!Fresh(100,301))result|=64;
var actual=good with{OverchargeState=2,CruiseParametersVerified=true,CruiseApproachSeconds=6,Distance=784.9268650541314*ScoApproachEnvelope.LightSecond,Speed=26.417687132075883*ScoApproachEnvelope.LightSecond};
if(CruiseOverchargeNativeGuard.Refusal(good with{TargetVerified=false},1100,1250) is not null
&&CruiseOverchargeNativeGuard.Refusal(actual,1100,1250) is null
&&CruiseOverchargeNativeGuard.Refusal(actual with{Distance=100*ScoApproachEnvelope.LightSecond},1100,1250)=="approach-or-alignment")result|=128;
foreach(var op in new[]{"activate","input.set","local.select","cruise.set"})
{using var request=JsonDocument.Parse(Obj(("op",op)).ToJsonString());try{Handle(request.RootElement);return result;}catch(InvalidOperationException){}}
result|=256;var status=Describe();if(!status["enabled"]!.GetValue<bool>()&&status["overchargeCommandsSent"]!.GetValue<ulong>()==0)result|=512;
var cycle=new OriginalCruiseCycleQualification();var identity=new OriginalCruiseCycleIdentity(new('a',32),new('b',32),1,2,3);
cycle.Begin(identity,1000,true,1);for(ulong i=1;i<=5;i++){cycle.Observe(2,3,1000+i*20,2);cycle.Observe(2,3,1000+i*20,2);}
if(cycle.ActiveSamples==5&&cycle.Stop(identity,new('c',32),1120,true,2,3,true)&&!cycle.Qualified)
{cycle.Observe(2,3,1140,3);if(cycle.Qualified)result|=1024;}
if(Navigation.TestCruiseMotionHistory())result|=2048;
return result;
}
}
internal static class CruiseControlExports
{
[UnmanagedCallersOnly(EntryPoint="XFEInteractionStart")]internal static uint Start(nuint thread){try{return CruiseControlRuntime.Start(thread);}catch{return 0;}}
[UnmanagedCallersOnly(EntryPoint="XFECruiseControlSelfTest")]internal static uint SelfTest(){try{return CruiseControlRuntime.SelfTest();}catch{return 0;}}
}
#endif