返回提交历史
Modified
XFEEDToolBox.Core/Execution/AutomaticTradeEngine.cs
+6
-0
Modified
XFEEDToolBox.Core/Navigation/NativeSystemFlightControlDriver.cs
+3
-3
Added
XFEEDToolBox.Core/Navigation/StationDepartureWatchdog.cs
+20
-0
Modified
XFEEDToolBox.Test/NativeTradeCommandDriverTests.cs
+24
-0
Added
XFEEDToolBox.Test/StationDepartureWatchdogTests.cs
+22
-0
Modified
docs/TRADE-WORK-COORDINATION.md
+7
-0
XFEstudio/XFEEDToolBox
Recover stalled station departures before the departure deadline
e42ea2a
代码差异
6 个文件
+82
-3
@@ -69,6 +69,7 @@ public sealed class AutomaticTradeEngine(AutomaticTradeConfiguration configurati
69
69
{
70
70
try{await driver.ExecuteAsync(new(TradeActionKind.Travel,new(){From=player().MarketId==configuration.Source.MarketId?configuration.Source:configuration.Destination,To=destination},null),CheckAsync,token).ConfigureAwait(false);return;}
71
71
catch(DockingHandoffException)when(attempt<3&&recoverToSameMode is not null){await RecoverAsync(token).ConfigureAwait(false);}
72
catch(StationDepartureRecoveryException)when(attempt<3&&recoverToSameMode is not null){await RecoverAsync(token).ConfigureAwait(false);}
72
73
catch(Exception error)when(NavigationRecovery.CanRetry(error,token))
73
74
{await StateAsync("travelling","正在衔接下一段航程,自动继续",destination,token);await Task.Delay(100,token).ConfigureAwait(false);}
74
75
}
@@ -120,6 +121,7 @@ public sealed class AutomaticTradeEngine(AutomaticTradeConfiguration configurati
120
121
journey.Status+=text=>Progress?.Invoke(new(phase,text,next,completed,profit,active));
121
122
try{await journey.RunAsync(leg,token).ConfigureAwait(false);break;}
122
123
catch(NativeTradeJourneyIncompleteException error)when(error.InnerException is DockingHandoffException&&attempt<3&&recoverToSameMode is not null){await RecoverAsync(token).ConfigureAwait(false);}
124
catch(NativeTradeJourneyIncompleteException error)when(error.InnerException is StationDepartureRecoveryException&&attempt<3&&recoverToSameMode is not null){await RecoverAsync(token).ConfigureAwait(false);}
123
125
catch(NativeTradeJourneyIncompleteException error)when(error.InnerException is {} cause&&NavigationRecovery.CanRetry(cause,token))
124
126
{await StateAsync("cargo-journey","正在衔接下一段航程,保留原货物继续",leg.To,token);await Task.Delay(100,token).ConfigureAwait(false);}
125
127
}
@@ -173,6 +175,10 @@ public sealed class AutomaticTradeEngine(AutomaticTradeConfiguration configurati
173
175
await CompleteAsync(path,leg,token).ConfigureAwait(false);
174
176
}
175
177
if(player().CargoCount!=0)throw new InvalidOperationException("当前已有货物,但没有唯一可恢复的订单,未新增采购。");
178
// Recovered launch failures can leave the same ship docked and damaged.
179
// Resolve all prior orders first, then finish station services before another departure.
180
if(player().Docked&&player().MarketId is {} resumedMarket)
181
await driver.ServiceDockedAsync(new(resumedMarket,initial.CargoCapacity),CheckAsync,token).ConfigureAwait(false);
176
182
foreach(var station in new[]{configuration.Source,configuration.Destination})
177
183
{
178
184
var file=Path.Combine(root,"quotes-"+station.MarketId+".json");
@@ -114,14 +114,14 @@ public sealed partial class NativeSystemFlightControlDriver(int processId,string
114
114
var station=catalogue.Locations.SingleOrDefault(x=>x.MarketId==initial.MarketId)??throw new FlightTelemetryUnavailableException("正在恢复当前停靠站点。");
115
115
var origin=new SystemNavigationTarget(catalogue.SystemAddress,station.WorldAddress,station.Name,SystemTargetKind.Station,station.MarketId)
116
116
{NativeKey=station.Key,NativeKind=station.NativeKind,NativeIdentity=station.Identity};
117
await SelectAsync(origin,token);var started=DateTimeOffset.UtcNow;await RequestLaunchAsync(token);
118
var engaged=false;var watch=Stopwatch.StartNew();
117
await SelectAsync(origin,token);var started=DateTimeOffset.UtcNow;var watch=Stopwatch.StartNew();await RequestLaunchAsync(token);
118
var engaged=false;
119
119
while(true)
120
120
{
121
121
token.ThrowIfCancellationRequested();var state=player();
122
122
if(state.CurrentShipId!=initial.CurrentShipId||state.LoadGameAt!=initial.LoadGameAt||state.CommanderId!=initial.CommanderId||NavigationSafety.UnhealthyHull(state)||NavigationSafety.HasExternalThreat(state))
123
123
throw new InvalidOperationException("离场中的飞船或安全状态改变。");
124
if(watch.Elapsed>TimeSpan.FromMinutes(6))throw new TimeoutException("自动离场未能完成,已交还控制。");
124
StationDepartureWatchdog.Check(initial.MarketId!.Value,watch.Elapsed);
125
125
try
126
126
{
127
127
var frame=await ReadSystemFlightAsync(token);
@@ -0,0 +1,20 @@
1
namespace XFEEDToolBox.Core.Navigation;
2
3
/// <summary>The original station grants five minutes after undocking. Leave time for menu recovery.</summary>
4
public static class StationDepartureWatchdog
5
{
6
public static readonly TimeSpan RecoveryAfter = TimeSpan.FromMinutes(3);
7
8
public static void Check(long marketId, TimeSpan elapsed)
9
{
10
if (elapsed >= RecoveryAfter)
11
throw new StationDepartureRecoveryException(marketId, elapsed);
12
}
13
}
14
15
public sealed class StationDepartureRecoveryException(long marketId, TimeSpan elapsed)
16
: TimeoutException("自动离场长时间未完成,正在离站期限前通过原始主菜单重进并继续原航程。")
17
{
18
public long MarketId { get; } = marketId;
19
public TimeSpan Elapsed { get; } = elapsed;
20
}
@@ -2,6 +2,7 @@ using System.Text.Json;
2
2
using XFEEDToolBox.Core.Execution;
3
3
using XFEEDToolBox.Core.Models;
4
4
using XFEEDToolBox.Core.Native;
5
using XFEEDToolBox.Core.Navigation;
5
6
using XFEExtension.NetCore.XUnit.Attributes;
6
7
7
8
namespace XFEEDToolBox.Test;
@@ -325,6 +326,29 @@ public class NativeTradeCommandDriverTests
325
326
var cycles=0;engine.Progress+=p=>{if(p.Phase=="cycle-completed")cycles++;};
326
327
await engine.RunAsync(default);Check.That(navigationCalls>=3);Check.Equal(2,cycles);Check.Equal(4,channel.Mutations);Check.Equal(0,channel.Cargo);Check.Equal(1L,channel.Player.MarketId!.Value);
327
328
}
329
[Test] public async Task DepartureQueueRecoveryContinuesEmptyOrLoadedWithoutRepeatingOrders()
330
{
331
foreach(var loaded in new[]{false,true})
332
{
333
using var directory=new TemporaryDirectory();var channel=new Channel{Credits=10000000,BatchSupported=true,FuelNeeded=false};
334
channel.Player=channel.Player with{CargoCapacity=752,Credits=channel.Credits,Ship="type9",Rebuy=1000};
335
var failed=false;var recovered=0;
336
var driver=new NativeTradeCommandDriver(channel,()=>channel.Player,Path.Combine(directory.Path,"operations"),
337
(station,_,_)=>
338
{
339
if(!failed&&(channel.Cargo>0)==loaded)
340
{failed=true;channel.Player=channel.Player with{Docked=false};throw new StationDepartureRecoveryException((long)channel.ActualMarket,TimeSpan.FromMinutes(3));}
341
channel.Player=channel.Player with{MarketId=station.MarketId,Docked=true};channel.ActualMarket=(ulong)station.MarketId;return Task.CompletedTask;
342
},_=>Task.CompletedTask,batchPolicy:new(752,1000));
343
driver.WalletObserved+=o=>channel.Player=o.Project(channel.Player);
344
var configuration=new AutomaticTradeConfiguration{Source=new(){MarketId=1,Name="One",SystemName="A"},Destination=new(){MarketId=99,Name="Other",SystemName="B"},WorkingDirectory=directory.Path};
345
Task Recover(CancellationToken _)
346
{recovered++;channel.Player=channel.Player with{Docked=true,LoadGameAt=channel.Player.LoadGameAt!.Value.AddSeconds(1)};return Task.CompletedTask;}
347
var engine=new AutomaticTradeEngine(configuration,driver,channel,()=>channel.Player,_=>Task.CompletedTask,directory.Path,Recover){MaximumCycles=1};
348
await engine.RunAsync(default);
349
Check.That(failed);Check.Equal(1,recovered);Check.Equal(2,channel.Mutations);Check.Equal(0,channel.Cargo);
350
}
351
}
328
352
[Test] public async Task CgEngineUsesUserDestinationAndConfirmsItsActualContribution()
329
353
{
330
354
using var directory=new TemporaryDirectory();var channel=new Channel{Credits=10000000,BatchSupported=true,FuelNeeded=false,Goals=true};
@@ -0,0 +1,22 @@
1
using XFEEDToolBox.Core.Navigation;
2
using XFEExtension.NetCore.XUnit.Attributes;
3
4
namespace XFEEDToolBox.Test;
5
6
[TestFixture]
7
public class StationDepartureWatchdogTests
8
{
9
[Test] public void NormalLaunchAndShortTrafficQueueCanComplete()
10
{
11
StationDepartureWatchdog.Check(123,TimeSpan.FromSeconds(50));
12
StationDepartureWatchdog.Check(123,TimeSpan.FromSeconds(179));
13
}
14
[Test] public void StuckLaunchRequestsRecoveryWithTwoMinutesBeforeStationPenalty()
15
{
16
Check.Equal(TimeSpan.FromMinutes(2),TimeSpan.FromMinutes(5)-StationDepartureWatchdog.RecoveryAfter);
17
try{StationDepartureWatchdog.Check(123,TimeSpan.FromMinutes(3));}
18
catch(StationDepartureRecoveryException error)
19
{Check.Equal(123L,error.MarketId);Check.Equal(TimeSpan.FromMinutes(3),error.Elapsed);return;}
20
throw new InvalidOperationException("Station departure did not request menu recovery before the penalty.");
21
}
22
}
@@ -2,6 +2,13 @@
2
2
3
3
更新:2026-10-25(Asia/Shanghai)。最新状态以紧接下方的记录为准,后文保留历史故障及验收过程。
4
4
5
## 自动离场排队超时修复及原会话恢复(2026-10-25 07:24)
6
7
- 原752t Gallite已完成售出;随后07:13:57工具箱自行从Polyakov空载离站,原游戏自动起飞持续显示排队等待、速度0,07:17:37与07:18:48后台原图分别确认剩余离站时间1:20与0:09。07:19:06原始HullDamage确认船体78.4498%;没有观察到Died。独立受损保护已退出并通过官方启动器重启97564→101440,不把一般危险标记当作这次实际受损的证据,也不把尚未发生的维修费用记入利润。
8
- 直接缺陷是自动离场的6分钟上限超过实际5分钟离站期限。现在从发送原起飞请求前开始单调计时,3分钟仍未完成则释放自己的原生起飞/轴控制,抛出专用恢复状态;工具箱通过原始主菜单重进原模式,保留同一目的地、货物及买卖检查点,覆盖空载和载货两种情况。正常排队可继续;不改受损退出、初始无盾或健康/身份/遥测条件。恢复次数受原恢复上限约束,不重放未知订单。
9
- 在途订单全部核对后,若启动时已停靠,先完成当前站点实际支持的燃油、维修、补给再离站。2项离站时限检查及30项原生交易驱动回归通过,包含空载/载货自动重进后各只采购、出售一次;Core Release构建0警告0错误。只更新工具箱r10/90604,加载器0.5.17、原ABI及当前游戏进程保留,没有重做已完成的迁移。
10
- 工具箱已自行在101440进入原Solo,07:24:02原始LoadGame确认Type-9/7、07:24:13原始Location确认Duamta;随后连续保护与原生预检通过,state恢复travelling、issue=null,07:24:45实际导航已进入跃迁。该时刻不是停靠、完整新批次或原始SCO完整验收。助手没有发送逐项游戏操作,没有定时任务和子智能体;程序循环属于工具箱,对话结束不取消。
11
5
12
## 归零回执瞬断改为只读观察;原752t已售出并继续(2026-10-25 07:16)
6
13
7
14
- 用户重申工具箱持续自动往返,助手只检测,出错停了才修复,不建定时任务。检查发现原98656在06:38:51因“Original reset outcome is unknown”终止流程,但原游戏随后06:40:50已实际停靠Polyakov;货物仍是原已确认买入的752t Gallite,不能再买同批。