XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFEEDToolBox

Keep ordinary flight running when optional SCO start is refused

Project native SCO collision and assist interlocks into the application policy before requesting boost. Isolate only matching, never-executed start refusals with fresh healthy/off observations; retain strict ownership, damage, takeover and stop confirmation checks. Use verified arrival ETA instead of sampled assist speed for active cruise braking. Add bounded SCO retry backoff, recorded-Jameson replay and named-pipe integration regressions. Publish0.4.30 with inactive window startup for UI-only updates. Validation:1,548 tests passed and six skipped; actual Jameson snapshot replay passed. Game restart and CG/navigation autostart are not part of this update.

94b9389
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

16 个文件 +215 -8
Modified README.md +2 -2
@@ -1,6 +1,6 @@
1 1 # XFE ED ToolBox
2 2
3 当前 0.4.29 修复 SCO 在 30 秒抵达时间附近过早停用并反复启动;普通空间的空间站进近改为 7.5 km 外固定全节流及原始推进器加速,进入范围立即归零并呼叫空管,直接落在 5 km 内同样处理。自动降落等待按用户要求放宽到未接管且进入 2 km 或超过 10 秒。改装等级保留游戏贴图,采用单层图像消除重影。详见 [0.4.29 说明](docs/INTERFACE-0.4.29.md)。
3 当前 0.4.30 修复星系内导航一走一停:将原生 SCO 几何联锁投影到加速策略,明确未执行的可恢复 SCO 启动拒绝只影响加速,不再释放普通驾驶。普通超巡不再因越过采样速度值而产生额外零节流脉冲,进站仍按原始进近时间减速。详见 [0.4.30 说明](docs/INTERFACE-0.4.30.md)。
4 4
5 5 0.4.23 修复当前星系原生 3D 合成层为零尺寸的空白问题,并将跑商改为全范围候选、并行计算、真实进度、采购来源类型筛选和按船型核对停机坪;CG 交付候选使用紫色及文字标识。详见 [0.4.23 说明](docs/INTERFACE-0.4.23.md)。当前星系仍支持真实坐标与左侧目录双向选择,实时商品保留报价详情、来源和完整采集时间;两页依据 Imagegen 设计稿实现。重复诊断会合并,跑商实际运行期间可恢复确切的游戏暂停菜单,手动暂停或停止任务后撤销恢复权限。既有范围见 [0.4.22 说明](docs/INTERFACE-0.4.22.md)及[当前星系与星系内驾驶](docs/CURRENT-SYSTEM-NAVIGATION.md)。
6 6
@@ -14,7 +14,7 @@ Elite Dangerous 的 WinUI 3 指挥官工具箱。当前版本提供实时数据
14 14
15 15 ## 运行
16 16
17 双击仓库根目录的 `Start-Toolbox.cmd`,优先启动 `dist/XFEEDToolBox-0.4.29-win-x64/XFEEDToolBox.exe`。星图与导航已合并;输入星系名称可自动补全,也可选中/双击星点聚焦,长按恒星待外圈填满后由游戏规划,确认成功后点击“开始自动导航”。航路由游戏计算,软件校验并读取。主页与飞船编辑背景采用本地真实船体的旋转科技线稿。发布包面向 Windows x64,包含 .NET 运行时与 Windows App SDK;源码构建需要 PowerShell 7、.NET 10 SDK、Windows SDK 和 Visual Studio 的 C++ x64 构建工具。`--page Navigation --maximized` 可直接打开最大化星图,`--page Refit` 打开飞船编辑。
17 双击仓库根目录的 `Start-Toolbox.cmd`,优先启动 `dist/XFEEDToolBox-0.4.30-win-x64/XFEEDToolBox.exe`。星图与导航已合并;输入星系名称可自动补全,也可选中/双击星点聚焦,长按恒星待外圈填满后由游戏规划,确认成功后点击“开始自动导航”。航路由游戏计算,软件校验并读取。主页与飞船编辑背景采用本地真实船体的旋转科技线稿。发布包面向 Windows x64,包含 .NET 运行时与 Windows App SDK;源码构建需要 PowerShell 7、.NET 10 SDK、Windows SDK 和 Visual Studio 的 C++ x64 构建工具。`--page Navigation --maximized` 可直接打开最大化星图,`--page Refit` 打开飞船编辑。
18 18
19 19 ```powershell
20 20 pwsh -File scripts/Build.ps1
Modified Start-Toolbox.cmd +8 -0
@@ -2,6 +2,14 @@
2 2 setlocal
3 3 cd /d "%~dp0"
4 4 if not defined XFE_TOOLBOX_DATA_DIR if exist "work\toolbox-local-data-dir.txt" set /p "XFE_TOOLBOX_DATA_DIR="<"work\toolbox-local-data-dir.txt"
5 if exist "dist\XFEEDToolBox-0.4.30-win-x64\XFEEDToolBox.exe" (
6 if defined XFE_TOOLBOX_DATA_DIR (
7 start "" "dist\XFEEDToolBox-0.4.30-win-x64\XFEEDToolBox.exe" %* --data-dir "%XFE_TOOLBOX_DATA_DIR%"
8 exit /b
9 )
10 start "" "dist\XFEEDToolBox-0.4.30-win-x64\XFEEDToolBox.exe" %*
11 exit /b
12 )
5 13 if exist "dist\XFEEDToolBox-0.4.29-win-x64\XFEEDToolBox.exe" (
6 14 if defined XFE_TOOLBOX_DATA_DIR (
7 15 start "" "dist\XFEEDToolBox-0.4.29-win-x64\XFEEDToolBox.exe" %* --data-dir "%XFE_TOOLBOX_DATA_DIR%"
Modified XFEEDToolBox.Core/Navigation/CruiseOverchargePolicy.cs +9 -0
@@ -10,6 +10,10 @@ public sealed record CruiseOverchargeObservation
10 10 public string GameSha256{get;init;}="";
11 11 public bool OriginalInterfacesVerified{get;init;}
12 12 public bool OriginalCanEngage{get;init;}
13 public bool NativeFlightConstraintsVerified{get;init;}
14 public bool NativeGeometryVerified{get;init;}
15 public bool NativeCollisionCourse{get;init;}
16 public bool NativeAssistEnabled{get;init;}
13 17 public CruiseOverchargeState State{get;init;}
14 18 public double HeatPercent{get;init;}=double.NaN;
15 19 public double HeatRisePercentPerSecond{get;init;}=double.NaN;
@@ -35,6 +39,8 @@ public sealed class CruiseOverchargePolicy
35 39 private DateTimeOffset blockedUntil;
36 40 private SystemNavigationTarget? approachTarget;
37 41 private bool finalApproach;
42 public void ObserveNativeStartRefusal(DateTimeOffset now)
43 {requesting=false;engagementRequestedAt=null;blockedUntil=now.AddSeconds(1);}
38 44 public void ObserveNativeApproachStop(SystemFlightFrame flight)
39 45 {
40 46 approachTarget=flight.SelectedTarget;ship=flight.ShipEntity;world=flight.ReferenceWorld;finalApproach=true;
@@ -60,6 +66,9 @@ public sealed class CruiseOverchargePolicy
60 66 ||flight.GameSha256.Length!=64||flight.GameSha256!=observed.GameSha256)return Off(CruiseOverchargeReason.Ownership);
61 67 static bool Fresh(DateTimeOffset at,DateTimeOffset current)=>at<=current&&current-at<=TimeSpan.FromMilliseconds(250);
62 68 if(!Fresh(flight.ObservedAt,now)||!Fresh(observed.ObservedAt,now))return Off(CruiseOverchargeReason.Telemetry);
69 if(!observed.NativeFlightConstraintsVerified)return Off(CruiseOverchargeReason.Telemetry);
70 if(observed.NativeAssistEnabled)return Off(CruiseOverchargeReason.AssistConflict);
71 if(!observed.NativeGeometryVerified||observed.NativeCollisionCourse)return Off(CruiseOverchargeReason.Geometry);
63 72 if(flight.SupercruiseAssist is NativeAssistState.Requested or NativeAssistState.Aligning or NativeAssistState.Engaged)
64 73 return Off(CruiseOverchargeReason.AssistConflict);
65 74 if(!flight.GeometryVerified||flight.SelectedTarget is null||flight.SystemAddress<=0||flight.SelectedTarget.SystemAddress!=flight.SystemAddress
Modified XFEEDToolBox.Core/Navigation/NativeCruiseControlChannel.cs +15 -1
@@ -17,6 +17,7 @@ public sealed class NativeCruiseControlChannel(int processId,string gameSha256)
17 17 private bool leaseRequested,originalStartWitness;
18 18 private string owner="";
19 19 private string? pendingStopId,lastStopIdentity,originalStartRequestId;
20 private DateTimeOffset nativeStartRefusedUntil;
20 21 public JsonElement? LastStatus{get;private set;}
21 22 public CruiseOverchargeObservation? LastObservation{get;private set;}
22 23 public CruiseOverchargeDecision? LastDecision{get;private set;}
@@ -108,6 +109,7 @@ public sealed class NativeCruiseControlChannel(int processId,string gameSha256)
108 109 var mayBoost=flight.Mode==LocalFlightMode.Supercruise&&(decision.Actions&~SystemNavigationAction.CancelOwnedAssists)==SystemNavigationAction.None
109 110 &&decision.Phase is SystemNavigationPhase.Supercruise or SystemNavigationPhase.Aligning;
110 111 if(!mayBoost){await ReleaseAsync(token);return;}
112 if(DateTimeOffset.UtcNow<nativeStartRefusedUntil&&!leaseRequested)return;
111 113 if(flight.GameSha256!=gameSha256||flight.ShipEntity==0||flight.ReferenceWorld==0||selectionGeneration==0)
112 114 throw new InvalidDataException("SCO requires the current original flight target and controller.");
113 115 if(entity!=flight.ShipEntity||world!=flight.ReferenceWorld)
@@ -154,7 +156,19 @@ public sealed class NativeCruiseControlChannel(int processId,string gameSha256)
154 156 Capture(result);
155 157 if(result.TryGetProperty("receipt",out var receipt)&&receipt.ValueKind==JsonValueKind.Object&&receipt.GetProperty("requestId").GetString()==requestId)
156 158 {
157 if(Flag(receipt,"failed"))throw new FlightTelemetryUnavailableException("Original SCO interlock refused: "+(receipt.TryGetProperty("reason",out var why)?why.GetString():"unknown"));
159 if(Flag(receipt,"failed"))
160 {
161 // A rejected optional start is not loss of ordinary flight
162 // telemetry. Only a matching, never-called, healthy/off
163 // receipt can leave the primary control lease running.
164 if(!originalStartWitness&&ScoStartRefusal.CanContinueOrdinaryFlight(result,receipt,owner,requestId,command,selectionGeneration,flight,DateTimeOffset.UtcNow))
165 {
166 leaseRequested=false;pendingStopId=null;nativeStartRefusedUntil=DateTimeOffset.UtcNow.AddSeconds(1);
167 policy.ObserveNativeStartRefusal(DateTimeOffset.UtcNow);
168 LastDecision=new(false,CruiseOverchargeReason.Unavailable);return;
169 }
170 throw new FlightTelemetryUnavailableException("Original SCO interlock refused: "+(receipt.TryGetProperty("reason",out var why)?why.GetString():"unknown"));
171 }
158 172 if(Flag(receipt,"applied"))
159 173 {
160 174 if(receipt.GetProperty("owner").GetString()!=owner||receipt.GetProperty("generation").GetUInt64()!=command
Modified XFEEDToolBox.Core/Navigation/NativeCruiseObservationChannel.cs +3 -0
@@ -35,6 +35,7 @@ public sealed class CruiseOverchargeSampleHistory(string expectedGameSha256)
35 35 {
36 36 var unavailable=new CruiseOverchargeObservation{GameSha256=expectedGameSha256,ShipEntity=flight.ShipEntity,ReferenceWorld=flight.ReferenceWorld};
37 37 static bool True(JsonElement e,string key)=>e.TryGetProperty(key,out var value)&&value.ValueKind==JsonValueKind.True;
38 static bool Boolean(JsonElement e,string key)=>e.TryGetProperty(key,out var value)&&value.ValueKind is JsonValueKind.True or JsonValueKind.False;
38 39 static double Number(JsonElement e,string key)=>e.TryGetProperty(key,out var value)&&value.ValueKind==JsonValueKind.Number&&value.TryGetDouble(out var number)?number:double.NaN;
39 40 static ulong Identity(JsonElement e,string key)=>e.TryGetProperty(key,out var value)&&value.ValueKind==JsonValueKind.String
40 41 &&ulong.TryParse(value.GetString(),NumberStyles.None,CultureInfo.InvariantCulture,out var id)?id:0;
@@ -72,6 +73,8 @@ public sealed class CruiseOverchargeSampleHistory(string expectedGameSha256)
72 73 }
73 74 var result=new CruiseOverchargeObservation{ObservedAt=at,GameSha256=expectedGameSha256,ShipEntity=entity,ReferenceWorld=world,
74 75 OriginalInterfacesVerified=true,OriginalCanEngage=True(sample,"nativeCanEngage"),State=(CruiseOverchargeState)(state+1),
76 NativeFlightConstraintsVerified=Boolean(sample,"geometryVerified")&&Boolean(sample,"collisionCourse")&&Boolean(sample,"nativeAssistEnabled"),
77 NativeGeometryVerified=True(sample,"geometryVerified"),NativeCollisionCourse=True(sample,"collisionCourse"),NativeAssistEnabled=True(sample,"nativeAssistEnabled"),
75 78 HeatPercent=heat,HeatRisePercentPerSecond=slope,FuelMainTonnes=fuelVerified?main:double.NaN,FuelCapacityTonnes=fuelVerified?capacity:double.NaN,
76 79 FuelBurnTonnesPerSecond=burn,FuelRateUpperTonnesPerSecond=upper,ThermalInput=ScoThermalJson.Read(sample,entity,world,at)};
77 80 previous=result;previousSample=sample.Clone();previousTotalFuel=total;return result;
Added XFEEDToolBox.Core/Navigation/ScoStartRefusal.cs +33 -0
@@ -0,0 +1,33 @@
1 using System.Text.Json;
2
3 namespace XFEEDToolBox.Core.Navigation;
4
5 /// <summary>Recognizes a completed optional SCO refusal without weakening primary flight checks.</summary>
6 public static class ScoStartRefusal
7 {
8 public static bool CanContinueOrdinaryFlight(JsonElement status,JsonElement receipt,string owner,string requestId,ulong generation,ulong selection,
9 SystemFlightFrame flight,DateTimeOffset now)
10 {
11 static bool Is(JsonElement e,string key,JsonValueKind kind)=>e.ValueKind==JsonValueKind.Object&&e.TryGetProperty(key,out var v)&&v.ValueKind==kind;
12 static string? Text(JsonElement e,string key)=>Is(e,key,JsonValueKind.String)?e.GetProperty(key).GetString():null;
13 static ulong Id(JsonElement e,string key)=>ulong.TryParse(Text(e,key),out var value)?value:0;
14 static bool Number(JsonElement e,string key,ulong expected)=>Is(e,key,JsonValueKind.Number)&&e.GetProperty(key).TryGetUInt64(out var value)&&value==expected;
15 if(!flight.InGame||!flight.GeometryVerified||flight.PlayerOverride||flight.ExternalThreat||flight.Mode!=LocalFlightMode.Supercruise||flight.ShipEntity==0||flight.ReferenceWorld==0
16 ||flight.ObservedAt>now||now-flight.ObservedAt>TimeSpan.FromMilliseconds(300)||string.IsNullOrEmpty(owner)||string.IsNullOrEmpty(requestId)
17 ||!Is(status,"owned",JsonValueKind.False)||!Is(receipt,"failed",JsonValueKind.True)||!Is(receipt,"applied",JsonValueKind.False)
18 ||!Is(receipt,"nativeMethodCalled",JsonValueKind.False)||Text(receipt,"owner")!=owner||Text(receipt,"requestId")!=requestId
19 ||generation==0||selection==0||!Number(receipt,"generation",generation)||!Number(receipt,"selectionGeneration",selection)
20 ||Text(receipt,"reason") is not("geometry-or-assist" or "approach-or-alignment" or "native-refusal" or "fuel" or "heat" or "thermal-model")
21 ||!status.TryGetProperty("sample",out var sample)||sample.ValueKind!=JsonValueKind.Object
22 ||Id(sample,"entityId")!=flight.ShipEntity||Id(sample,"referenceWorldAddress")!=flight.ReferenceWorld
23 ||!Is(sample,"verified",JsonValueKind.True)||!Is(sample,"inSupercruise",JsonValueKind.True)||!Is(sample,"stableFsd",JsonValueKind.True)
24 ||!Number(sample,"rawFsdState",2)||!Is(sample,"playerOverride",JsonValueKind.False)||!Is(sample,"state",JsonValueKind.Number)
25 ||!sample.GetProperty("state").TryGetInt32(out var stateId)||stateId is not(0 or 3)
26 ||!Is(sample,"observedAtUnixMilliseconds",JsonValueKind.Number)||!sample.GetProperty("observedAtUnixMilliseconds").TryGetInt64(out var milliseconds)
27 ||milliseconds>now.ToUnixTimeMilliseconds()||milliseconds<now.ToUnixTimeMilliseconds()-200
28 ||!receipt.TryGetProperty("gate",out var gate)||!Is(gate,"identityVerified",JsonValueKind.True)||!Is(gate,"targetVerified",JsonValueKind.True)
29 ||!Is(gate,"emergency",JsonValueKind.False)||!Is(gate,"damageSeen",JsonValueKind.False)||!Is(gate,"assistEnabled",JsonValueKind.False)
30 ||!Is(gate,"hullFraction",JsonValueKind.Number)||!gate.GetProperty("hullFraction").TryGetDouble(out var fraction)||!double.IsFinite(fraction)||fraction is <=0 or >1)return false;
31 return true;
32 }
33 }
Modified XFEEDToolBox.Core/Navigation/SystemFlightNavigator.cs +4 -1
@@ -271,7 +271,10 @@ public sealed class SystemFlightNavigator
271 271 {
272 272 if(!frame.CruiseParametersVerified||!double.IsFinite(frame.CruiseApproachSeconds)||frame.CruiseApproachSeconds<=0
273 273 ||!double.IsFinite(frame.CruiseApproachThrottle)||frame.CruiseApproachThrottle is <=0 or >1)return 0;
274 var allowed=Math.Min(frame.MaximumCruiseSpeed,Math.Max(frame.SafeDropSpeed*.8,frame.TargetDistance/frame.CruiseApproachSeconds));
274 // The game already enforces its sampled speed limit. Do not issue a
275 // zero-throttle pulse for crossing it; active approach braking uses
276 // the verified target arrival window instead.
277 var allowed=Math.Max(frame.SafeDropSpeed*.8,frame.TargetDistance/frame.CruiseApproachSeconds);
275 278 // Throttle is a position in the original speed controller. Hold its
276 279 // configured approach setting; normal game deceleration handles the
277 280 // speed curve. Separate brake/release thresholds prevent switching
Modified XFEEDToolBox.Test/CruiseOverchargePolicyTests.cs +8 -1
@@ -14,12 +14,19 @@ public class CruiseOverchargePolicyTests
14 14 Mode=LocalFlightMode.Supercruise,Position=new(0,0,0),TargetPosition=new(0,0,1e12),Forward=new(0,0,1),TargetDistance=1e12,Speed=1e9,
15 15 SelectedTarget=new(1,9,"Test Station",SystemTargetKind.Station,10),SupercruiseAssist=NativeAssistState.Unavailable};
16 16 private static CruiseOverchargeObservation Observation=>new(){ObservedAt=Now,ShipEntity=7,ReferenceWorld=8,GameSha256=Hash,
17 OriginalInterfacesVerified=true,OriginalCanEngage=true,State=CruiseOverchargeState.Idle,HeatPercent=40,HeatRisePercentPerSecond=0,
17 OriginalInterfacesVerified=true,OriginalCanEngage=true,NativeFlightConstraintsVerified=true,NativeGeometryVerified=true,State=CruiseOverchargeState.Idle,HeatPercent=40,HeatRisePercentPerSecond=0,
18 18 FuelMainTonnes=32,FuelCapacityTonnes=32,FuelBurnTonnesPerSecond=0,FuelRateUpperTonnesPerSecond=1,ThermalInput=Thermal};
19 19 private static CruiseOverchargeDecision Decide(CruiseOverchargeObservation? sample=null,SystemFlightFrame? flight=null,double reserve=8)
20 20 =>new CruiseOverchargePolicy().Decide(flight??Flight,sample??Observation,reserve,Now);
21 21
22 22 [Test]public void VerifiedCoolAlignedDistantShipCanRequestBoost()=>Check.That(Decide().UseOvercharge);
23 [Test]public void NativeScoConstraintsStopOnlyBoostAndCanRecoverWithoutChangingThePrimaryFrame()
24 {
25 foreach(var blocked in new[]{Observation with{NativeCollisionCourse=true},Observation with{NativeGeometryVerified=false},Observation with{NativeAssistEnabled=true},Observation with{NativeFlightConstraintsVerified=false}})
26 Check.That(!Decide(blocked).UseOvercharge);
27 var policy=new CruiseOverchargePolicy();Check.Equal(CruiseOverchargeReason.Geometry,policy.Decide(Flight,Observation with{NativeCollisionCourse=true},8,Now).Reason);
28 Check.That(policy.Decide(Flight,Observation,8,Now).UseOvercharge);
29 }
23 30 [Test]public void TheObserved785LightSecondCaseContinuesWithTheVerifiedOriginalApproachWindow()
24 31 {
25 32 const double ls=299792458;var distance=784.9268650541314*ls;
Modified XFEEDToolBox.Test/CruiseOverchargeSampleHistoryTests.cs +9 -0
@@ -14,9 +14,18 @@ public class CruiseOverchargeSampleHistoryTests
14 14 private static JsonElement Sample(DateTimeOffset at,double heat=40,double reserve=.4,ulong entity=12,ulong world=24,int rawFsd=2,bool bound=true,bool ready=true,int state=0,bool canEngage=true)
15 15 =>JsonSerializer.SerializeToElement(new{enabled=true,sampleReady=ready,gameSha256=Hash,sample=new{verified=true,entityId=entity.ToString(),referenceWorldAddress=world.ToString(),
16 16 observedAtUnixMilliseconds=at.ToUnixTimeMilliseconds(),inSupercruise=true,stableFsd=true,rawFsdState=rawFsd,state,nativeCanEngage=canEngage,heatPercent=heat,
17 geometryVerified=true,collisionCourse=false,nativeAssistEnabled=false,
17 18 fuelVerified=true,fuelMainTonnes=32,fuelCapacityTonnes=32,reserveFuelTonnes=reserve,fuelBoundVerified=bound,fuelRateUpperTonnesPerSecond=1}});
18 19 [Test]public void FirstSampleCannotInventHeatOrFuelSlopes()
19 20 {var value=new CruiseOverchargeSampleHistory(Hash).Read(Sample(Now),Flight,Now);Check.That(value.OriginalInterfacesVerified&&double.IsNaN(value.HeatRisePercentPerSecond)&&double.IsNaN(value.FuelBurnTonnesPerSecond));}
21 [Test]public void NativeCollisionAndAssistFactsSurviveProjectionInsteadOfBeingReplacedByThePrimaryDefaults()
22 {
23 var node=JsonNode.Parse(Sample(Now).GetRawText())!;node["sample"]!["collisionCourse"]=true;node["sample"]!["nativeAssistEnabled"]=true;
24 var projected=new CruiseOverchargeSampleHistory(Hash).Read(JsonSerializer.SerializeToElement(node),Flight,Now);
25 Check.That(projected.NativeFlightConstraintsVerified&&projected.NativeGeometryVerified&&projected.NativeCollisionCourse&&projected.NativeAssistEnabled);
26 node["sample"]!.AsObject().Remove("geometryVerified");
27 Check.That(!new CruiseOverchargeSampleHistory(Hash).Read(JsonSerializer.SerializeToElement(node),Flight,Now).NativeFlightConstraintsVerified);
28 }
20 29 [Test]public void TwoSamplesUseBothTheMainAndReservoirFuel()
21 30 {
22 31 var history=new CruiseOverchargeSampleHistory(Hash);history.Read(Sample(Now),Flight,Now);var later=Now.AddMilliseconds(100);
Modified XFEEDToolBox.Test/EmptyStationValidationTests.cs +1 -1
@@ -18,7 +18,7 @@ public class EmptyStationValidationTests
18 18 {Sequence=1,ObservedAt=at,GameSha256=Hash,ShipEntity=entity,ReferenceWorld=world,SelectedTarget=Target,SystemAddress=10,InGame=true,GeometryVerified=true,Mode=mode,
19 19 HeatPercent=45,TargetDistance=6000,SupercruiseAssist=NativeAssistState.Disabled,Forward=new(0,0,1),Up=new(0,1,0)};
20 20 private static CruiseOverchargeObservation Observation(DateTimeOffset at,CruiseOverchargeState state=CruiseOverchargeState.Active)=>new()
21 {ObservedAt=at,ShipEntity=12,ReferenceWorld=24,GameSha256=Hash,OriginalInterfacesVerified=true,OriginalCanEngage=true,State=state,HeatPercent=45,HeatRisePercentPerSecond=1,
21 {ObservedAt=at,ShipEntity=12,ReferenceWorld=24,GameSha256=Hash,OriginalInterfacesVerified=true,OriginalCanEngage=true,NativeFlightConstraintsVerified=true,NativeGeometryVerified=true,State=state,HeatPercent=45,HeatRisePercentPerSecond=1,
22 22 FuelMainTonnes=31,FuelCapacityTonnes=32,FuelBurnTonnesPerSecond=.1,FuelRateUpperTonnesPerSecond=.3};
23 23 private static OriginalCruiseFuelRange Range(DateTimeOffset at)=>new(12,24,at,true,CruiseFuelRangeScope.OriginalScoConsumptionCurve,.05,.3);
24 24 private static OriginalOverchargeReceipt Command(DateTimeOffset at,bool enable,long sequence=1,bool original=true)=>new(Guid.NewGuid().ToString("N"),11,111,Hash,Owner,1,12,24,sequence,at,at,at.AddMilliseconds(250),enable,true,original){CommandGeneration=sequence};
Added XFEEDToolBox.Test/NativeCruiseStartRefusalTests.cs +58 -0
@@ -0,0 +1,58 @@
1 using System.IO.Pipes;
2 using System.Text.Json;
3 using XFEEDToolBox.Core.Navigation;
4 using XFEExtension.NetCore.XUnit.Attributes;
5
6 namespace XFEEDToolBox.Test;
7
8 [TestFixture]
9 public class NativeCruiseStartRefusalTests
10 {
11 [Test]public async Task ARefusedOptionalStartReturnsToPrimaryControlAndBacksOffInsteadOfSuspendingIt()
12 {
13 var pid=Random.Shared.Next(500000,900000);var hash=new string('a',64);var calls=0;var starts=0;JsonElement? receipt=null;
14 using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(6));
15 var server=Task.Run(async()=>
16 {
17 while(!timeout.IsCancellationRequested)
18 {
19 try
20 {
21 await using var pipe=new NamedPipeServerStream($"XFEEDToolBox.{NativeCruiseControlChannel.Service}.{pid}",PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous);
22 await pipe.WaitForConnectionAsync(timeout.Token);var buffer=new byte[8192];var count=await pipe.ReadAsync(buffer,timeout.Token);
23 using var parsed=JsonDocument.Parse(buffer.AsMemory(0,count));var request=parsed.RootElement;var op=request.GetProperty("op").GetString();calls++;
24 if(op=="cruise.set")
25 {
26 starts++;receipt=JsonSerializer.SerializeToElement(new{owner=request.GetProperty("owner").GetString(),requestId=request.GetProperty("requestId").GetString(),generation=1,
27 selectionGeneration=1,failed=true,applied=false,nativeMethodCalled=false,reason="geometry-or-assist",gate=new{identityVerified=true,targetVerified=true,emergency=false,damageSeen=false,assistEnabled=false,hullFraction=1}});
28 }
29 else Check.That(op is "watch" or "read","An off SCO refusal must not send a stop or a primary input command.");
30 var now=DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
31 var result=new{schema=1,service=NativeCruiseControlChannel.Service,processId=pid,gameSha256=hash,enabled=true,owningThread=7,overchargeControl=true,
32 originalFuelRangeObservations=true,nativeStopReceipts=true,componentThermalModel=true,expiringOriginalControl=true,controlTelemetrySchema=1,overchargeControlQualified=false,
33 owned=false,generation=1,receipt,sampleReady=true,sample=new{verified=true,entityId="12",referenceWorldAddress="24",observedAtUnixMilliseconds=now,
34 inSupercruise=true,stableFsd=true,rawFsdState=2,state=0,nativeCanEngage=true,playerOverride=false,geometryVerified=true,collisionCourse=false,nativeAssistEnabled=false,
35 heatPercent=40,fuelVerified=true,fuelMainTonnes=32,fuelCapacityTonnes=32,reserveFuelTonnes=.4,fuelBoundVerified=true,fuelRateUpperTonnesPerSecond=1,
36 thermal=new{schema=1,verified=true,entityId="12",referenceWorldAddress="24",sourceObservedAtUnixMilliseconds=now,heatObservedAtUnixMilliseconds=now,
37 heatCapacity=100,heatDisplayScale=1,scoInputUpper=10,otherInputUpper=3,coolingPassive=10,coolingMaximum=10,coolingScale=1,coolingHeatLower=0,coolingHeatUpper=1,responseDelaySeconds=.02}}};
38 await pipe.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(new{ok=true,result}),timeout.Token);await pipe.WriteAsync(new byte[]{10},timeout.Token);
39 if(OperatingSystem.IsWindows())pipe.WaitForPipeDrain();
40 }
41 catch(OperationCanceledException)when(timeout.IsCancellationRequested){break;}
42 }
43 });
44 var channel=new NativeCruiseControlChannel(pid,hash);var target=new SystemNavigationTarget(1,24,"Station",SystemTargetKind.Station,42){NativeKey=1,NativeKind=9};
45 async Task Update()
46 {
47 var now=DateTimeOffset.UtcNow;var frame=new SystemFlightFrame{ObservedAt=now,Sequence=1,ShipEntity=12,ReferenceWorld=24,GameSha256=hash,InGame=true,
48 Mode=LocalFlightMode.Supercruise,GeometryVerified=true,SystemAddress=1,SelectedTarget=target,Forward=new(0,0,1),TargetPosition=new(0,0,1e12),TargetDistance=1e12,Speed=1e9};
49 await channel.UpdateAsync("controller",1,frame,new(SystemNavigationPhase.Supercruise,"ordinary flight",new(0,0,1),.75),target,8,now.AddMilliseconds(250),timeout.Token);
50 }
51 try
52 {
53 await Update();await Task.Delay(35,timeout.Token);await Update();Check.Equal(1,starts);
54 var before=calls;await Update();Check.Equal(before,calls);Check.That(channel.LastDecision?.UseOvercharge==false);
55 }
56 finally{timeout.Cancel();await server;}
57 }
58 }
Added XFEEDToolBox.Test/ScoStartRefusalTests.cs +44 -0
@@ -0,0 +1,44 @@
1 using System.Text.Json;
2 using System.Text.Json.Nodes;
3 using XFEEDToolBox.Core.Navigation;
4 using XFEExtension.NetCore.XUnit.Attributes;
5
6 namespace XFEEDToolBox.Test;
7
8 [TestFixture]
9 public class ScoStartRefusalTests
10 {
11 private static readonly DateTimeOffset Now=DateTimeOffset.FromUnixTimeMilliseconds(1700000000000);
12 private static readonly SystemFlightFrame Frame=new(){InGame=true,GeometryVerified=true,Mode=LocalFlightMode.Supercruise,ShipEntity=12,ReferenceWorld=24,ObservedAt=Now};
13 private static JsonObject Status()=>JsonSerializer.SerializeToNode(new{owned=false,sample=new{verified=true,entityId="12",referenceWorldAddress="24",inSupercruise=true,
14 stableFsd=true,rawFsdState=2,playerOverride=false,state=0,observedAtUnixMilliseconds=Now.ToUnixTimeMilliseconds()},receipt=new{owner="owner",requestId="request",generation=7,
15 selectionGeneration=8,failed=true,applied=false,nativeMethodCalled=false,reason="geometry-or-assist",gate=new{identityVerified=true,targetVerified=true,emergency=false,damageSeen=false,
16 assistEnabled=false,hullFraction=1.0}}})!.AsObject();
17 private static bool Allowed(JsonObject status,SystemFlightFrame? frame=null)
18 {var value=JsonSerializer.SerializeToElement(status);return ScoStartRefusal.CanContinueOrdinaryFlight(value,value.GetProperty("receipt"),"owner","request",7,8,frame??Frame,Now);}
19 [Test]public void MatchingNeverStartedCollisionRefusalPreservesOrdinaryFlight()
20 {
21 Check.That(Allowed(Status()));
22 foreach(var reason in new[]{"approach-or-alignment","native-refusal","fuel","heat","thermal-model"})
23 {var status=Status();status["receipt"]!["reason"]=reason;Check.That(Allowed(status));}
24 }
25 [Test]public void OwnedActiveForeignOrUnacknowledgedScoCannotBeTreatedAsAnOptionalRefusal()
26 {
27 foreach(var alter in new Action<JsonObject>[]
28 {s=>s["owned"]=true,s=>s["sample"]!["state"]=2,s=>s["sample"]!["entityId"]="99",s=>s["sample"]!["referenceWorldAddress"]="99",
29 s=>s["receipt"]!["owner"]="foreign",s=>s["receipt"]!["requestId"]="foreign",s=>s["receipt"]!["generation"]=6,s=>s["receipt"]!["selectionGeneration"]=6,
30 s=>s["receipt"]!["nativeMethodCalled"]=true,s=>s["receipt"]!["applied"]=true,s=>s["receipt"]!["failed"]=false,s=>s["receipt"]!.AsObject().Remove("nativeMethodCalled")})
31 {var status=Status();alter(status);Check.That(!Allowed(status));}
32 }
33 [Test]public void ActualSafetyIdentityTelemetryAndAssistFailuresStillInterruptControl()
34 {
35 foreach(var alter in new Action<JsonObject>[]
36 {s=>s["receipt"]!["reason"]="health",s=>s["receipt"]!["reason"]="identity-or-target",s=>s["receipt"]!["gate"]!["emergency"]=true,s=>s["receipt"]!["gate"]!["damageSeen"]=true,
37 s=>s["receipt"]!["gate"]!["hullFraction"]=0,s=>s["receipt"]!["gate"]!["assistEnabled"]=true,s=>s["sample"]!["playerOverride"]=true,
38 s=>s["sample"]!["observedAtUnixMilliseconds"]=Now.AddSeconds(-1).ToUnixTimeMilliseconds(),s=>s["sample"]!["observedAtUnixMilliseconds"]=long.MinValue,s=>s["sample"]!["state"]="bad",
39 s=>s["receipt"]!["gate"]!["hullFraction"]="bad"})
40 {var status=Status();alter(status);Check.That(!Allowed(status));}
41 Check.That(!Allowed(Status(),Frame with{PlayerOverride=true}));Check.That(!Allowed(Status(),Frame with{ExternalThreat=true}));
42 Check.That(!Allowed(Status(),Frame with{ObservedAt=Now.AddSeconds(-1)}));
43 }
44 }
Modified XFEEDToolBox.Test/SystemNavigationTests.cs +7 -0
Modified XFEEDToolBox/App.xaml.cs +2 -1
Modified XFEEDToolBox/XFEEDToolBox.csproj +1 -1
Added docs/INTERFACE-0.4.30.md +11 -0