返回提交历史
Modified
README.md
+2
-2
Modified
Start-Toolbox.cmd
+8
-0
Modified
XFEEDToolBox.Core/Navigation/CruiseOverchargePolicy.cs
+9
-0
Modified
XFEEDToolBox.Core/Navigation/NativeCruiseControlChannel.cs
+15
-1
Modified
XFEEDToolBox.Core/Navigation/NativeCruiseObservationChannel.cs
+3
-0
Added
XFEEDToolBox.Core/Navigation/ScoStartRefusal.cs
+33
-0
Modified
XFEEDToolBox.Core/Navigation/SystemFlightNavigator.cs
+4
-1
Modified
XFEEDToolBox.Test/CruiseOverchargePolicyTests.cs
+8
-1
Modified
XFEEDToolBox.Test/CruiseOverchargeSampleHistoryTests.cs
+9
-0
Modified
XFEEDToolBox.Test/EmptyStationValidationTests.cs
+1
-1
Added
XFEEDToolBox.Test/NativeCruiseStartRefusalTests.cs
+58
-0
Added
XFEEDToolBox.Test/ScoStartRefusalTests.cs
+44
-0
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
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
代码差异
16 个文件
+215
-8
@@ -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
@@ -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%"
@@ -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&¤t-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
@@ -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
@@ -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;
@@ -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
}
@@ -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
@@ -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;
@@ -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);
@@ -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};
@@ -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
}
@@ -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
}