返回提交历史
Added
XFEEDToolBox.Core/Native/NativeDockingInputDiagnostics.cs
+56
-0
Modified
XFEEDToolBox.Core/Navigation/TravelPreparation.cs
+10
-1
Modified
XFEEDToolBox.Test/TravelPreparationTests.cs
+20
-1
Modified
docs/CG-TRADING-VALIDATION.md
+4
-0
Modified
research/NativeAcceptance/Program.cs
+7
-0
XFEstudio/XFEEDToolBox
Wait for fresh input observer samples and expose read-only docking diagnostics
A newly loaded interaction observer can refuse its first input sample. Retry known transient reads within the existing preparation deadline, without sending inputs or relaxing ship identity and deployment checks. Add a bounded read-only FSD/docking action-registry provenance report. Live evidence confirms both registries are the same; the continuous SCA-to-docking handoff remains unresolved and protected exits stay enabled. Validation: isolated acceptance CLI publish; all 8 TravelPreparation tests pass, including transient startup, terminal ownership failure and cancellation.
b5b7ba4
代码差异
5 个文件
+97
-2
@@ -0,0 +1,56 @@
1
using System.Diagnostics;
2
using System.Globalization;
3
using XFEEDToolBox.Core.Navigation;
4
5
namespace XFEEDToolBox.Core.Native;
6
7
/// <summary>Read-only provenance check for the original FSD and docking action registries.</summary>
8
public static class NativeDockingInputDiagnostics
9
{
10
public static async Task<object> ReadAsync(int processId,CancellationToken token=default)
11
{
12
using var process=Process.GetProcessById(processId);
13
var started=process.StartTime.ToUniversalTime().Ticks;
14
var frame=await new NavigationPipeClient(processId).CallAsync(new{op="read"},token);
15
var entity=ulong.Parse(frame.GetProperty("entityId").GetString()!,CultureInfo.InvariantCulture);
16
using var memory=new ReadOnlyPlayerMemory(processId,started,NativeSystemLocationReader.GameSha256);
17
var (fsd,life,root)=memory.Player(entity);
18
var computer=memory.Component(root,memory.U32(memory.ImageBase+0x5F5317C));
19
if(memory.U64(computer)!=memory.ImageBase+0x5165248)throw new InvalidDataException("Original docking computer differs.");
20
var fsdController=memory.U64(fsd+0x700);var dockingController=memory.U64(computer+0x240);
21
if(fsdController==0||fsdController!=dockingController||memory.U64(memory.U64(fsdController)+0x158)!=memory.ImageBase+0x25E5D10)
22
throw new InvalidDataException("Original throttle controller ownership differs.");
23
var fsdActions=memory.U64(fsd+0x6F8);var dockingActions=memory.U64(computer+0x230);
24
object Registry(ulong value)
25
{
26
if(memory.U64(value)!=memory.ImageBase+0x512FD90||memory.U64(memory.U64(value)+0x40)!=memory.ImageBase+0x8FEDC0)
27
throw new InvalidDataException("Original action registry differs.");
28
var buckets=memory.U64(value+0xC0);var count=memory.U64(value+0xC8);
29
if(buckets==0||count is 0 or >4096)throw new InvalidDataException("Original action registry exceeds bound.");
30
var seen=new HashSet<ulong>();object? zero=null;
31
for(ulong i=0;i<count;i++)
32
{
33
var node=memory.U64(buckets+i*8);
34
while(node<buckets||node>=buckets+count*8)
35
{
36
if(node==0||!seen.Add(node)||seen.Count>4096)throw new InvalidDataException("Original action registry chain differs.");
37
var name=memory.U64(node+8);var length=memory.U64(name);
38
if(length is 0 or >128)throw new InvalidDataException("Original action name exceeds bound.");
39
if(System.Text.Encoding.ASCII.GetString(memory.Read(name+0x14,(int)length))=="SetSpeedZero")
40
{
41
var binding=memory.U64(node+0x10);var bytes=memory.Read(binding,0x38);
42
zero=new{binding=binding.ToString("x"),pending=BitConverter.ToInt32(bytes,0x1C),forced=bytes[0x24],
43
consumed=BitConverter.ToInt32(bytes,0x2C),consumedForced=bytes[0x34]};
44
}
45
node=memory.U64(node);
46
}
47
}
48
if(memory.U64(value+0xC0)!=buckets||memory.U64(value+0xC8)!=count)throw new InvalidDataException("Original action registry changed.");
49
return new{address=value.ToString("x"),zero};
50
}
51
var originalFsd=Registry(fsdActions);var originalDocking=Registry(dockingActions);
52
memory.Validate();if(memory.U64(life+0xB0)!=entity)throw new InvalidDataException("Main ship identity changed.");
53
return new{entity=entity.ToString(),fsd=fsd.ToString("x"),computer=computer.ToString("x"),sameRegistry=fsdActions==dockingActions,
54
controllerThrottle=BitConverter.ToSingle(memory.Read(fsdController+0x680,4)),originalFsd,originalDocking,processAccess="QUERY_INFORMATION | VM_READ",mutationsSent=0};
55
}
56
}
@@ -31,7 +31,16 @@ public static class TravelPreparation
31
31
{
32
32
token.ThrowIfCancellationRequested();var state=player();
33
33
if(!state.InGame||state.Docked||NavigationSafety.HasExternalThreat(state))throw new InvalidOperationException("收起设备时游戏会话或飞行安全状态已改变。");
34
var frame=await channel.ReadAsync(token);var now=DateTimeOffset.UtcNow;
34
GameInputState frame;
35
try{frame=await channel.ReadAsync(token);}
36
catch(Exception error)when(NavigationRecovery.CanRetry(error,token))
37
{
38
// A newly loaded input observer can refuse its first sample.
39
// Keep the same overall bound and send nothing until it is
40
// fresh and belongs to the selected main ship.
41
clearSince=null;await Task.Delay(40,token);continue;
42
}
43
var now=DateTimeOffset.UtcNow;
35
44
if(frame.Sequence<=0||frame.EntityId==0||frame.ObservedAt>now.AddMilliseconds(100)||now-frame.ObservedAt>TimeSpan.FromMilliseconds(300))throw new FlightTelemetryUnavailableException("收起设备的原生状态已过期。");
36
45
if(expectedEntity is {} expected&&expected!=frame.EntityId)throw new InvalidOperationException("输入通道与当前飞船身份不一致。");
37
46
if(!StatusConfirmed(state,now))
@@ -14,8 +14,10 @@ public class TravelPreparationTests
14
14
public readonly List<string> Writes=[];
15
15
public Action? OnWrite;
16
16
public bool Refresh=true;
17
public int RefusedReads,Reads;
18
public string Refusal="Game input telemetry unavailable or stale";
17
19
public Task ConnectAsync(int thread,CancellationToken token)=>Task.CompletedTask;
18
public Task<GameInputState> ReadAsync(CancellationToken token){if(Refresh)Frame=Frame with{Sequence=Frame.Sequence+1,ObservedAt=DateTimeOffset.UtcNow};return Task.FromResult(Frame);}
20
public Task<GameInputState> ReadAsync(CancellationToken token){Reads++;if(RefusedReads-->0)throw new InvalidOperationException(Refusal);if(Refresh)Frame=Frame with{Sequence=Frame.Sequence+1,ObservedAt=DateTimeOffset.UtcNow};return Task.FromResult(Frame);}
19
21
public Task<long> SetAsync(GameInputState frame,string input,bool value,CancellationToken token)
20
22
{
21
23
Check.That(!value);Writes.Add(input);OnWrite?.Invoke();
@@ -36,6 +38,23 @@ public class TravelPreparationTests
36
38
var c=new Channel{Frame=new(1,42,DateTimeOffset.UtcNow,false,false,false)};
37
39
await TravelPreparation.EnsureRetractedAsync(c,()=>Player(0),CancellationToken.None,42);Check.Equal(0,c.Writes.Count);
38
40
}
41
[Test]public async Task NewObserverWaitsForFreshInputWithoutSendingAnyToggle()
42
{
43
var c=new Channel{Frame=new(1,42,DateTimeOffset.UtcNow,false,false,false),RefusedReads=2};
44
using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(2));
45
await TravelPreparation.EnsureRetractedAsync(c,()=>Player(0),stop.Token,42);
46
Check.That(c.Reads>=4);Check.Equal(0,c.Writes.Count);
47
}
48
[Test]public async Task UnknownInputRefusalIsTerminalAndKnownWaitingCanBeCancelled()
49
{
50
var c=new Channel{RefusedReads=1,Refusal="Input observer ownership changed"};
51
try{await TravelPreparation.EnsureRetractedAsync(c,()=>Player(0),CancellationToken.None,42);throw new Exception("Expected ownership refusal");}
52
catch(InvalidOperationException){Check.Equal(1,c.Reads);Check.Equal(0,c.Writes.Count);}
53
c.RefusedReads=int.MaxValue;c.Refusal="Game input telemetry unavailable or stale";
54
using var stop=new CancellationTokenSource(150);
55
try{await TravelPreparation.EnsureRetractedAsync(c,()=>Player(0),stop.Token,42);throw new Exception("Expected cancellation");}
56
catch(OperationCanceledException){Check.Equal(0,c.Writes.Count);}
57
}
39
58
[Test]public async Task UnchangedStatusRemainsValidWhenFreshlyReadAndBelongsToTheCurrentSession()
40
59
{
41
60
var now=DateTimeOffset.UtcNow;
@@ -16,6 +16,10 @@
16
16
17
17
原始返回菜单路径未通过实机;只有插件明确声明 `nativeReturnToMenuVerified=true` 才允许该路径。从已就绪主菜单进入Solo使用独立入口能力。
18
18
19
后续空载连续航程再次完成起飞、跃迁、SCA和空管,但自动降落仍未接管并触发保护退出。实际飞行油门持续为.75,即便控制器零位及原始输入回执已确认。只读来源诊断确认FSD与自动降落使用同一个原始输入注册表,因此不能把来源差异当作已确认原因。可使用 `inspect-docking-input-sources <PID> <新报告>` 读取来源与原始零输入待消费/已消费状态;该命令仅使用QUERY_INFORMATION和VM_READ权限。
20
21
新会话加载交互观察器时,首个输入样本可能暂未发布。准备流程现在在原有12秒上限内重读已知暂时拒绝;在新鲜输入、正确船舶和物理收起状态全部确认前不发送输入。未知所有权拒绝和取消仍立即停止。
22
19
23
## 运行与恢复
20
24
21
25
先通过 `scripts/Build-Plugin.ps1` 构建原生包,使用游戏插件管理器安装;游戏构建哈希变化后必须重新适配。再构建 `research/NativeAcceptance/NativeAcceptance.csproj`。研究CLI仅在已授权且身份核对过的游戏会话使用,不模拟键鼠。
@@ -7,6 +7,13 @@ using XFEEDToolBox.Core.Galaxy;
7
7
8
8
try
9
9
{
10
if(args.Length==3&&args[0]=="inspect-docking-input-sources")
11
{
12
if(File.Exists(args[2]))throw new IOException("Use a new read-only docking input report.");
13
var result=await XFEEDToolBox.Core.Native.NativeDockingInputDiagnostics.ReadAsync(int.Parse(args[1]));
14
await File.WriteAllTextAsync(args[2],JsonSerializer.Serialize(result,ToolboxSettings.JsonOptions));
15
Console.WriteLine(JsonSerializer.Serialize(result,ToolboxSettings.JsonOptions));return;
16
}
10
17
if(args.Length==3&&args[0]=="start-session")
11
18
{
12
19
try{await GameSessionAcceptance.StartAsync(args[1],args[2]);}