#if PLUGIN_CRUISE_CONTROL || PLUGIN_CRUISE_OBSERVATION
namespace XFEEDToolBox.Plugin;
internal static unsafe partial class Navigation
{
internal static JsonObject? ObserveScoThermal(in Snapshot frame,nuint sco)
{
if(!ReadSharedScoThermal(out var source))return Obj(("verified",false),("reason","main-loader-thermal-unavailable"));
var now=UtcMilliseconds();var root=sco-0x58;
var same=source.Entity==frame.entity&&source.World==frame.referenceWorld&&source.HeatManager==frame.heatManager;
var fresh=source.ObservedAtUnixMilliseconds<=now&&now-source.ObservedAtUnixMilliseconds<=200;
var component=sco>=0x10000&&Pointer(sco)==image+0x533E6E0&&Pointer(root)==image+0x533E5B0
&&Pointer(Pointer(sco)+0x20)==image+0x2EAACD0&&Pointer(root+0x168)==frame.heatManager+0x60;
var multiplier=double.NaN;var pressure=double.NaN;
var provider=component?Pointer(root+0x258):0;
if(provider>=0x10000&&Pointer(provider)==image+0x532A288&&Pointer(Pointer(provider)+0x20)==image+0x2DCDC40)
{
var module=Pointer(provider+0x5B8);var detail=module>=0x10000?Pointer(module+0x60):0;
var inner=detail>=0x10000?Pointer(detail+8)&~(nuint)3:0;
var entry=inner>=0x10000?Pointer(inner+0x30):0;var parameters=entry>=0x10000?Pointer(entry+0x30):0;
if(parameters>=0x10000){multiplier=Read<float>(parameters+0x214);pressure=Read<float>(root+0x298);}
}
var parametersReady=double.IsFinite(multiplier)&&multiplier>=0&&double.IsFinite(pressure)&&pressure is >=0 and <=1;
var future=default(XFEEDToolBox.Core.Navigation.OriginalCurveRange);var current=default(XFEEDToolBox.Core.Navigation.OriginalCurveRange);
var curveReady=component&&CruiseCurveBound(root+0x2E0,out future,out _,out _);
// A point interval retains the original interpolation/rounding bounds. The full interval is a future pressure envelope, not the current rate.
var pointReady=curveReady&¶metersReady&&ScoThermalCurvePoint(root+0x2E0,pressure,out current);
var scoUpper=curveReady&¶metersReady?Math.Max(0,future.Maximum)*multiplier:double.NaN;
// Read the shared pool again on this owning-thread sample. A previously published pre-radiator value is not the current baseline.
var storedNow=same&&component?ScoThermalEncoded(frame.heatManager+0x190):double.NaN;
var storedAt=UtcMilliseconds();
var storedHeat=source.HeatCapacity>0&&source.HeatDisplayScale>0?storedNow*100/(source.HeatCapacity*source.HeatDisplayScale):double.NaN;
var rows=new JsonArray();
for(var index=0;index<source.SourceCount;index++)
{
var row=source.Sources[index];rows.Add((JsonNode)Obj(("sourceId",row.Id.ToString("x")),("label",ScoThermalSourceLabel((nuint)row.Id)),
("verifiedSco",row.VerifiedSco!=0),("requestedEnergyPerSecond",row.RequestedRate),("upperEnergyPerSecond",row.UpperRate)));
}
static JsonNode? Number(double value)=>double.IsFinite(value)?JsonValue.Create(value):null;
var verified=same&&fresh&&source.Ready!=0&&component&&curveReady&¶metersReady&&pointReady&&double.IsFinite(scoUpper)
&&double.IsFinite(storedNow)&&storedNow>=0&&double.IsFinite(storedHeat)&&storedHeat>=0;
return Obj(("schema",1),("verified",verified),("entityId",source.Entity.ToString()),("referenceWorldAddress",source.World.ToString()),
("sourceObservedAtUnixMilliseconds",source.ObservedAtUnixMilliseconds),("heatObservedAtUnixMilliseconds",frame.utcMilliseconds),
("heatPercent",HeatHudPercent(frame)),("heatCapacity",source.HeatCapacity),("heatDisplayScale",source.HeatDisplayScale),
("otherInputUpper",source.OtherInputUpper),("scoInputUpper",Number(scoUpper)),("scoObservedInputUpper",source.ScoObservedInputUpper),
("coolingPassive",source.CoolingPassive),("coolingMaximum",source.CoolingMaximum),("coolingScale",source.CoolingScale*.97),
("nativeCoolingScale",source.CoolingScale),("coolingLowerBoundFactor",.97),
("coolingBoundScope","original radiator curve times native scale times 0.97; matches the existing minimum actual-to-curve verification ratio"),
("coolingHeatLower",source.CoolingHeatLower),("coolingHeatUpper",source.CoolingHeatUpper),("coolingActual",source.CoolingActual),
("coolingActive",source.CoolingActive!=0),("nativeHeatStepSeconds",source.NativeHeatStepSeconds),
("sourceWindowSimulationSeconds",source.WindowSimulationSeconds),("totalRequestedEnergyPerSecond",source.TotalRequestedRate),
("pressure",Number(pressure)),("scoHeatMultiplier",Number(multiplier)),("scoCurveMinimum",curveReady?Number(future.Minimum):null),
("scoCurveMaximum",curveReady?Number(future.Maximum):null),("scoCurrentRateMinimum",pointReady?Number(Math.Max(0,current.Minimum)*multiplier):null),
("scoCurrentRateMaximum",pointReady?Number(Math.Max(0,current.Maximum)*multiplier):null),
("storedEnergy",source.StoredEnergy),("storedEnergyHeatPercent",source.HeatCapacity>0&&source.HeatDisplayScale>0?Number(source.StoredEnergy*100/(source.HeatCapacity*source.HeatDisplayScale)):null),
("currentStoredEnergy",Number(storedNow)),("currentStoredEnergyHeatPercent",Number(storedHeat)),("currentStoredEnergyObservedAtUnixMilliseconds",storedAt),
("sources",rows),("sourceAccounting","requested-energy-upper"),("environmentSeparated",false),
("otherBoundScope","positive per-source observed envelopes over the last 0.2 simulation seconds; unidentified sources retained"),
("scoBoundScope","original 2E0 curve pressure 0..1 times module heat multiplier +214; SCO-off future contribution is zero"));
}
private static bool ScoThermalCurvePoint(nuint curve,double pressure,out XFEEDToolBox.Core.Navigation.OriginalCurveRange value)
{
value=default;var mode=Read<int>(curve);var count=Read<ulong>(curve+0x10);var data=Pointer(curve+0x18);
if(count is 0 or >256||data<0x10000)return false;
var points=new XFEEDToolBox.Core.Navigation.OriginalCurveKnot[(int)count];
for(var index=0;index<points.Length;index++)
{var at=data+(nuint)index*20;points[index]=new(Read<float>(at),Read<float>(at+4),Read<float>(at+12),Read<float>(at+16));}
return XFEEDToolBox.Core.Navigation.OriginalCurveBounds.TryBound(points,mode,pressure,pressure,out value);
}
}
#endif
#if PLUGIN_CRUISE_CONTROL || PLUGIN_CRUISE_OBSERVATION
namespace XFEEDToolBox.Plugin;
internal static unsafe partial class Navigation
{
internal static JsonObject? ObserveScoThermal(in Snapshot frame,nuint sco)
{
if(!ReadSharedScoThermal(out var source))return Obj(("verified",false),("reason","main-loader-thermal-unavailable"));
var now=UtcMilliseconds();var root=sco-0x58;
var same=source.Entity==frame.entity&&source.World==frame.referenceWorld&&source.HeatManager==frame.heatManager;
var fresh=source.ObservedAtUnixMilliseconds<=now&&now-source.ObservedAtUnixMilliseconds<=200;
var component=sco>=0x10000&&Pointer(sco)==image+0x533E6E0&&Pointer(root)==image+0x533E5B0
&&Pointer(Pointer(sco)+0x20)==image+0x2EAACD0&&Pointer(root+0x168)==frame.heatManager+0x60;
var multiplier=double.NaN;var pressure=double.NaN;
var provider=component?Pointer(root+0x258):0;
if(provider>=0x10000&&Pointer(provider)==image+0x532A288&&Pointer(Pointer(provider)+0x20)==image+0x2DCDC40)
{
var module=Pointer(provider+0x5B8);var detail=module>=0x10000?Pointer(module+0x60):0;
var inner=detail>=0x10000?Pointer(detail+8)&~(nuint)3:0;
var entry=inner>=0x10000?Pointer(inner+0x30):0;var parameters=entry>=0x10000?Pointer(entry+0x30):0;
if(parameters>=0x10000){multiplier=Read<float>(parameters+0x214);pressure=Read<float>(root+0x298);}
}
var parametersReady=double.IsFinite(multiplier)&&multiplier>=0&&double.IsFinite(pressure)&&pressure is >=0 and <=1;
var future=default(XFEEDToolBox.Core.Navigation.OriginalCurveRange);var current=default(XFEEDToolBox.Core.Navigation.OriginalCurveRange);
var curveReady=component&&CruiseCurveBound(root+0x2E0,out future,out _,out _);
// A point interval retains the original interpolation/rounding bounds. The full interval is a future pressure envelope, not the current rate.
var pointReady=curveReady&¶metersReady&&ScoThermalCurvePoint(root+0x2E0,pressure,out current);
var scoUpper=curveReady&¶metersReady?Math.Max(0,future.Maximum)*multiplier:double.NaN;
// Read the shared pool again on this owning-thread sample. A previously published pre-radiator value is not the current baseline.
var storedNow=same&&component?ScoThermalEncoded(frame.heatManager+0x190):double.NaN;
var storedAt=UtcMilliseconds();
var storedHeat=source.HeatCapacity>0&&source.HeatDisplayScale>0?storedNow*100/(source.HeatCapacity*source.HeatDisplayScale):double.NaN;
var rows=new JsonArray();
for(var index=0;index<source.SourceCount;index++)
{
var row=source.Sources[index];rows.Add((JsonNode)Obj(("sourceId",row.Id.ToString("x")),("label",ScoThermalSourceLabel((nuint)row.Id)),
("verifiedSco",row.VerifiedSco!=0),("requestedEnergyPerSecond",row.RequestedRate),("upperEnergyPerSecond",row.UpperRate)));
}
static JsonNode? Number(double value)=>double.IsFinite(value)?JsonValue.Create(value):null;
var verified=same&&fresh&&source.Ready!=0&&component&&curveReady&¶metersReady&&pointReady&&double.IsFinite(scoUpper)
&&double.IsFinite(storedNow)&&storedNow>=0&&double.IsFinite(storedHeat)&&storedHeat>=0;
return Obj(("schema",1),("verified",verified),("entityId",source.Entity.ToString()),("referenceWorldAddress",source.World.ToString()),
("sourceObservedAtUnixMilliseconds",source.ObservedAtUnixMilliseconds),("heatObservedAtUnixMilliseconds",frame.utcMilliseconds),
("heatPercent",HeatHudPercent(frame)),("heatCapacity",source.HeatCapacity),("heatDisplayScale",source.HeatDisplayScale),
("otherInputUpper",source.OtherInputUpper),("scoInputUpper",Number(scoUpper)),("scoObservedInputUpper",source.ScoObservedInputUpper),
("coolingPassive",source.CoolingPassive),("coolingMaximum",source.CoolingMaximum),("coolingScale",source.CoolingScale*.97),
("nativeCoolingScale",source.CoolingScale),("coolingLowerBoundFactor",.97),
("coolingBoundScope","original radiator curve times native scale times 0.97; matches the existing minimum actual-to-curve verification ratio"),
("coolingHeatLower",source.CoolingHeatLower),("coolingHeatUpper",source.CoolingHeatUpper),("coolingActual",source.CoolingActual),
("coolingActive",source.CoolingActive!=0),("nativeHeatStepSeconds",source.NativeHeatStepSeconds),
("sourceWindowSimulationSeconds",source.WindowSimulationSeconds),("totalRequestedEnergyPerSecond",source.TotalRequestedRate),
("pressure",Number(pressure)),("scoHeatMultiplier",Number(multiplier)),("scoCurveMinimum",curveReady?Number(future.Minimum):null),
("scoCurveMaximum",curveReady?Number(future.Maximum):null),("scoCurrentRateMinimum",pointReady?Number(Math.Max(0,current.Minimum)*multiplier):null),
("scoCurrentRateMaximum",pointReady?Number(Math.Max(0,current.Maximum)*multiplier):null),
("storedEnergy",source.StoredEnergy),("storedEnergyHeatPercent",source.HeatCapacity>0&&source.HeatDisplayScale>0?Number(source.StoredEnergy*100/(source.HeatCapacity*source.HeatDisplayScale)):null),
("currentStoredEnergy",Number(storedNow)),("currentStoredEnergyHeatPercent",Number(storedHeat)),("currentStoredEnergyObservedAtUnixMilliseconds",storedAt),
("sources",rows),("sourceAccounting","requested-energy-upper"),("environmentSeparated",false),
("otherBoundScope","positive per-source observed envelopes over the last 0.2 simulation seconds; unidentified sources retained"),
("scoBoundScope","original 2E0 curve pressure 0..1 times module heat multiplier +214; SCO-off future contribution is zero"));
}
private static bool ScoThermalCurvePoint(nuint curve,double pressure,out XFEEDToolBox.Core.Navigation.OriginalCurveRange value)
{
value=default;var mode=Read<int>(curve);var count=Read<ulong>(curve+0x10);var data=Pointer(curve+0x18);
if(count is 0 or >256||data<0x10000)return false;
var points=new XFEEDToolBox.Core.Navigation.OriginalCurveKnot[(int)count];
for(var index=0;index<points.Length;index++)
{var at=data+(nuint)index*20;points[index]=new(Read<float>(at),Read<float>(at+4),Read<float>(at+12),Read<float>(at+16));}
return XFEEDToolBox.Core.Navigation.OriginalCurveBounds.TryBound(points,mode,pressure,pressure,out value);
}
}
#endif