XFEstudio/XFEEDToolBox
Move native trade execution into the trade planning page
Replace the separate execution navigation entry with one automatic execution button beside the route planner. Recalculate the current request before execution and discard the previous route on calculation failure. Keep progress, next stop and confirmed service-adjusted profit on the same page, with cancellation on the initiating button. Read verified docked-commerce capabilities from the native handshake instead of the obsolete package manifest flag. Retain mandatory flight prerequisites before procurement; this does not claim SCO, escape-vector control or the complete trading journey has passed. Validation: WinUI Release build with zero warnings/errors; eight native capability regressions; background screenshots, OCR and read-only UI checks. No trade or flight actions were issued for UI verification.
5071580
代码差异
@@ -0,0 +1,27 @@
using System.Text.Json;
using XFEEDToolBox.Core.Native;
namespace XFEEDToolBox.Core.Execution;
public static class NativeTradeCapabilities
{
public static bool SupportsDockedTrade(JsonElement description) =>
description.ValueKind == JsonValueKind.Object
&& description.TryGetProperty("schema", out var schema) && schema.ValueKind == JsonValueKind.Number && schema.TryGetInt32(out var version) && version == 1
&& Text(description, "name") == "XFEEDToolBox.Navigation"
&& Flag(description, "verified")
&& Text(description, "gameSha256") == NativeSystemLocationReader.GameSha256
&& Flag(description, "commerceDockedReadiness")
&& Flag(description, "cgCommodityBatches");
public static void RequireProductionFlight(JsonElement description)
{
if (!SupportsDockedTrade(description))
throw new NotSupportedException("当前游戏的原生交易接口尚未验证,采购尚未启动。");
if (!Flag(description, "supercruiseOverchargeControl") || !Flag(description, "interdictionEscapeControl"))
throw new NotSupportedException("SCO温控、燃油控制和原始逃逸矢量尚未全部就绪,采购尚未启动。");
}
private static bool Flag(JsonElement value, string name) => value.TryGetProperty(name, out var flag) && flag.ValueKind == JsonValueKind.True;
private static string? Text(JsonElement value, string name) => value.TryGetProperty(name, out var text) && text.ValueKind == JsonValueKind.String ? text.GetString() : null;
}
@@ -0,0 +1,41 @@
using System.Text.Json;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Native;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class NativeTradeCapabilitiesTests
{
private static Dictionary<string, object> Handshake => new()
{
["schema"] = 1, ["name"] = "XFEEDToolBox.Navigation", ["verified"] = true,
["gameSha256"] = NativeSystemLocationReader.GameSha256,
["commerceDockedReadiness"] = true, ["cgCommodityBatches"] = true
};
private static bool Ready(Dictionary<string, object> value) => NativeTradeCapabilities.SupportsDockedTrade(JsonSerializer.SerializeToElement(value));
[Test] public void VerifiedNativeCapabilitiesEnableTradeWithoutTheLegacyManifestFlag() => Check.Equal(true, Ready(Handshake));
[Test] public void AnUnverifiedInterfaceCannotEnableExecution()
{ var value = Handshake; value["verified"] = false; Check.Equal(false, Ready(value)); }
[Test] public void AnotherGameBuildCannotEnableExecution()
{ var value = Handshake; value["gameSha256"] = new string('0', 64); Check.Equal(false, Ready(value)); }
[Test] public void MissingBatchCapabilityCannotEnableExecution()
{ var value = Handshake; value.Remove("cgCommodityBatches"); Check.Equal(false, Ready(value)); }
[Test] public void MissingDockedReadinessCannotEnableExecution()
{ var value = Handshake; value["commerceDockedReadiness"] = false; Check.Equal(false, Ready(value)); }
[Test] public void AStringInsteadOfACapabilityIsNotAccepted()
{ var value = Handshake; value["cgCommodityBatches"] = "true"; Check.Equal(false, Ready(value)); }
[Test] public void DockedCommerceAloneCannotStartAProductionFlight()
{
try { NativeTradeCapabilities.RequireProductionFlight(JsonSerializer.SerializeToElement(Handshake)); }
catch (NotSupportedException) { return; }
throw new InvalidOperationException("The original flight prerequisites were bypassed.");
}
[Test] public void ProductionFlightRequiresBothOriginalControlCapabilities()
{
var value = Handshake; value["supercruiseOverchargeControl"] = true; value["interdictionEscapeControl"] = true;
NativeTradeCapabilities.RequireProductionFlight(JsonSerializer.SerializeToElement(value));
}
}
@@ -1,4 +1,4 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
@@ -79,7 +79,7 @@ public partial class ToolboxViewModel
RefreshNavigationStartHint();
StartNavigationCommand.NotifyCanExecuteChanged();StopNavigationCommand.NotifyCanExecuteChanged();
StartGameCommand.NotifyCanExecuteChanged();RelogGameCommand.NotifyCanExecuteChanged();RestartGameCommand.NotifyCanExecuteChanged();
StartPlanCommand.NotifyCanExecuteChanged();
RefreshTradePlanCommands();
});
}
partial void OnNavigationDestinationChanged(string value){if(initialized)RefreshNavigation(CurrentPlayer);}
@@ -238,7 +238,7 @@ public partial class ToolboxViewModel
if(!CanStartNavigation())return;
using var startStop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token);navigationStartStop=startStop;
NavigationActive=true;RefreshNavigationStartHint();StopNavigationCommand.NotifyCanExecuteChanged();
StartGameCommand.NotifyCanExecuteChanged();StartPlanCommand.NotifyCanExecuteChanged();RelogGameCommand.NotifyCanExecuteChanged();RestartGameCommand.NotifyCanExecuteChanged();
StartGameCommand.NotifyCanExecuteChanged();RefreshTradePlanCommands();RelogGameCommand.NotifyCanExecuteChanged();RestartGameCommand.NotifyCanExecuteChanged();
LogNavigation("启动导航",$"收到启动请求:{NavigationDestination.Trim()}");
try
{
@@ -252,7 +252,7 @@ public partial class ToolboxViewModel
catch(OperationCanceledException)when(lifetime.IsCancellationRequested){}
catch(OperationCanceledException)when(startStop.IsCancellationRequested){NavigationStatus="自动导航已停止。";NavigationStatusSeverity=InfoBarSeverity.Warning;LogNavigation("导航取消",NavigationStatus);}
catch(Exception e){NavigationStatus=e.Message;NavigationStatusSeverity=InfoBarSeverity.Error;LogNavigation("导航未启动",e.Message);}
finally{navigationStartStop=null;NavigationActive=false;RefreshNavigationStartHint();StartGameCommand.NotifyCanExecuteChanged();StartNavigationCommand.NotifyCanExecuteChanged();StopNavigationCommand.NotifyCanExecuteChanged();StartPlanCommand.NotifyCanExecuteChanged();RelogGameCommand.NotifyCanExecuteChanged();RestartGameCommand.NotifyCanExecuteChanged();}
finally{navigationStartStop=null;NavigationActive=false;RefreshNavigationStartHint();StartGameCommand.NotifyCanExecuteChanged();StartNavigationCommand.NotifyCanExecuteChanged();StopNavigationCommand.NotifyCanExecuteChanged();RefreshTradePlanCommands();RelogGameCommand.NotifyCanExecuteChanged();RestartGameCommand.NotifyCanExecuteChanged();}
}
private bool CanStopNavigation()=>NavigationActive;
[RelayCommand(CanExecute=nameof(CanStopNavigation))] private void StopNavigation(){navigationStartStop?.Cancel();runtime.Navigator.Stop();}
@@ -1,3 +1,4 @@
using XFEEDToolBox.Core.Execution;
using System.Diagnostics;
using System.Text.RegularExpressions;
using CommunityToolkit.Mvvm.ComponentModel;
@@ -66,7 +67,16 @@ public partial class ToolboxViewModel
await runtime.BindFlightDriverAsync(new NativeFlightControlDriver(game.Id,state.Installation!.GameSha256,()=>runtime.Player,()=>runtime.Settings.NavigationFuelReserve,
new NativeGameInputChannel(game.Id,state.Installation.GameSha256,Path.Combine(AppContext.BaseDirectory,"PluginPackage"))),lifetime.Token);
}
PluginTradeReady=!state.PendingRemoval&&hello.Loaded&&hello.Active&&hello.SupportsTrade&&pluginManager.Package().SupportsTrade;
if (!state.PendingRemoval && hello.Loaded && hello.Active && runtime.FlightDriver is NativeFlightControlDriver tradeFlight)
{
try
{
var capabilities = await new NativeCommerceChannel(tradeFlight.ProcessId).CallAsync(new { op = "describe" }, lifetime.Token);
PluginTradeReady = NativeTradeCapabilities.SupportsDockedTrade(capabilities);
}
catch (OperationCanceledException) when (lifetime.IsCancellationRequested) { throw; }
catch (Exception error) { runtime.Log("交易接口", error.Message); }
}
PluginNavigationReady=!state.PendingRemoval&&hello.Loaded&&hello.Active&&hello.SupportsNavigation&&pluginManager.Package().SupportsNavigation&&runtime.FlightDriver is not null;
PluginStatusText=$"插件已加载 · PID {game.Id} · {hello.Version}\n"+(hello.Active?"基础通信可用。\n"+(PluginNavigationReady?"自动导航驱动已就绪。":"等待进入飞行状态并完成原生遥测握手。")+"\n"+(PluginTradeReady?"自动贸易接口已就绪。":"自动贸易与底层重进适配尚未完成。"):"插件已停用,加载文件等待游戏退出后清理。");
NavigationNativeStatus=PluginNavigationReady?"原生导航已连接:姿态、实时热量、恒星边界、目标锁定和充能等待联锁。":hello.Active?"插件已加载,等待飞行状态和原生遥测。":"插件已停用。";
@@ -127,9 +137,9 @@ public partial class ToolboxViewModel
if(!PluginNavigationReady||runtime.FlightDriver is not NativeFlightControlDriver driver)
throw new InvalidOperationException("插件导航接口尚未就绪,工具箱暂不发出贸易或飞行指令。");
var hello=await new NavigationPipeClient(driver.ProcessId).CallAsync(new{op="describe"},lifetime.Token);
if(!hello.TryGetProperty("commerceDockedReadiness",out var docking)||docking.ValueKind!=System.Text.Json.JsonValueKind.True
||!hello.TryGetProperty("cgCommodityBatches",out var batches)||batches.ValueKind!=System.Text.Json.JsonValueKind.True)
if(!NativeTradeCapabilities.SupportsDockedTrade(hello))
throw new NotSupportedException("当前插件尚未提供经过验证的停靠交易接口。");
NativeTradeCapabilities.RequireProductionFlight(hello);
PluginTradeReady=true;
}
}
@@ -126,8 +126,10 @@ public partial class ToolboxViewModel : ViewModelBase, IDisposable
[ObservableProperty] public partial int ExecutionModeIndex { get; set; }
[ObservableProperty] public partial string ExecutionHint { get; set; } = "模拟演练只展示步骤,不向游戏发送输入。";
[ObservableProperty] public partial string RunStatus { get; set; } = "等待路线";
[ObservableProperty] public partial string RunMessage { get; set; } = "先计算路线,再选择执行方式。";
[ObservableProperty] public partial string RunMessage { get; set; } = "选择起点和参数,点击自动执行即可开始;也可先预览路线。";
[ObservableProperty] public partial string ActualProfit { get; set; } = "0 Cr";
[ObservableProperty] public partial string ProfitLabel { get; set; } = "已确认净利润";
[ObservableProperty] public partial string ProfitDetails { get; set; } = "按实际成交和停靠服务回执更新。";
[ObservableProperty] public partial double RunPercent { get; set; }
[ObservableProperty] public partial string NextDestination { get; set; } = "—";
[ObservableProperty] public partial string JournalDirectory { get; set; } = "";
@@ -189,9 +191,12 @@ public partial class ToolboxViewModel : ViewModelBase, IDisposable
RunStatus = progress.State switch { RunState.Running => "执行中", RunState.Paused => "已暂停", RunState.Completed => "已完成", RunState.Stopped => "已停止", RunState.Failed => "需要处理", _ => "等待路线" };
RunMessage = progress.Message;
ActualProfit = $"{progress.ActualProfit:N0} Cr";
ProfitLabel = progress.IsSimulation ? "模拟预计净收益" : "已确认净利润";
var costs = progress.Accounting;
ProfitDetails = progress.IsSimulation ? "规划估计值,未发生实际交易。" : $"燃油 {costs.Fuel:N0} · 维修 {costs.Repairs:N0} · 补给 {costs.Rearm:N0} · 其他支出 {costs.OtherExpenses:N0} Cr\n在途货物成本 {costs.CargoInvestment:N0} Cr · 现金变动 {costs.CashChange:+#,0;-#,0;0} Cr";
RunPercent = progress.State == RunState.Completed ? 100 : progress.TotalLegs == 0 ? 0 : (progress.Leg - 1d) / progress.TotalLegs * 100;
NextDestination = Plan?.Legs.ElementAtOrDefault(Math.Max(0, progress.Leg - 1))?.To.DisplayName ?? "—";
StartPlanCommand.NotifyCanExecuteChanged(); CalculateCommand.NotifyCanExecuteChanged();
RefreshTradePlanCommands(); CalculateCommand.NotifyCanExecuteChanged();
});
public async Task InitializeAsync()
@@ -234,12 +239,19 @@ public partial class ToolboxViewModel : ViewModelBase, IDisposable
FittedModules.Apply(value?.Modules ?? []);
SelectedShipSummary = value == null ? "没有飞船装配记录。" : $"{value.DisplayName} · {value.Ident}\n{value.Location} · {value.ConfigurationStatus}\n货舱 {value.CargoCapacity?.ToString() ?? "—"} t · 日志最大跳距 {value.MaxJumpRange?.ToString("F2") ?? "—"} ly · 重购 {value.Rebuy?.ToString("N0") ?? "—"} Cr\n最大跳距不是满载跳距,规划时仍需按载重校准。";
}
partial void OnPlanChanged(RoutePlan? value) => StartPlanCommand.NotifyCanExecuteChanged();
partial void OnPlanChanged(RoutePlan? value)
{
RefreshTradePlanCommands();
if (RunActive) return;
RunStatus = value?.Legs.Count > 0 ? "待执行" : "等待路线";
RunMessage = value?.Legs.Count > 0 ? "路线已准备,点击自动执行开始跑商。" : "选择起点和参数,点击自动执行即可开始;也可先预览路线。";
NextDestination = value?.Legs.FirstOrDefault()?.To.DisplayName ?? "—";
}
partial void OnExecutionModeIndexChanged(int value) => ExecutionHint = value switch
{
0 => "模拟演练只展示步骤,不向游戏发送输入。",
1 => "你在游戏中操作;工具按 Journal 自动确认买入、到站、卖出并推进路线。",
_ => "自动贸易必须安装并加载游戏插件;当前通信版本尚未启用未校准的原生交易和飞行。"
_ => "自动执行由工具箱完成采购、航行、出售及站内维护,并核对实际回执。"
};
public void ShowError(string message) => dispatcher.TryEnqueue(() => { ErrorMessage = message; HasError = true; });
@@ -341,10 +353,10 @@ public partial class ToolboxViewModel : ViewModelBase, IDisposable
partial void OnSessionBusyChanged(bool value)
{
StartGameCommand.NotifyCanExecuteChanged(); RelogGameCommand.NotifyCanExecuteChanged(); RestartGameCommand.NotifyCanExecuteChanged();
StartPlanCommand.NotifyCanExecuteChanged(); CalculateCommand.NotifyCanExecuteChanged();
RefreshTradePlanCommands(); CalculateCommand.NotifyCanExecuteChanged();
StartNavigationCommand.NotifyCanExecuteChanged();
}
partial void OnRunActiveChanged(bool value) { StartGameCommand.NotifyCanExecuteChanged(); RelogGameCommand.NotifyCanExecuteChanged(); RestartGameCommand.NotifyCanExecuteChanged();StartNavigationCommand.NotifyCanExecuteChanged(); }
partial void OnRunActiveChanged(bool value) { RefreshTradePlanCommands(); StartGameCommand.NotifyCanExecuteChanged(); RelogGameCommand.NotifyCanExecuteChanged(); RestartGameCommand.NotifyCanExecuteChanged();StartNavigationCommand.NotifyCanExecuteChanged(); }
private bool CanManageSession() => initialized && !DemoMode && !RunActive && !SessionBusy&&!NavigationActive;
private bool CanRelog() => CanManageSession() && sessions.CanRelog;
[RelayCommand(CanExecute = nameof(CanManageSession))] private Task StartGameAsync() => RunSessionAsync(SessionOperation.Start);
@@ -400,6 +412,7 @@ public partial class ToolboxViewModel : ViewModelBase, IDisposable
private bool CanImportData() => !IsBusy;
partial void OnIsBusyChanged(bool value)
{
RefreshTradePlanCommands();
CalculateCommand.NotifyCanExecuteChanged();ImportSystemCommand.NotifyCanExecuteChanged();
ImportSpanshCommand.NotifyCanExecuteChanged();ImportCaptureCommand.NotifyCanExecuteChanged();
}
@@ -457,13 +470,13 @@ public partial class ToolboxViewModel : ViewModelBase, IDisposable
if (ExecutionModeIndex is < 0 or > 2) throw new InvalidOperationException("请选择有效的执行方式。");
var nativeDriver=ExecutionModeIndex==2?await CreateNativeTradeDriverAsync(Plan,token):null;
token.ThrowIfCancellationRequested();
AppShellPage.Current?.Navigate(typeof(ExecutionPage));
AppShellPage.Current?.Navigate(typeof(PlannerPage));
RunActive = true;
await runtime.Runner.RunAsync(Plan, (ExecutionMode)ExecutionModeIndex, nativeDriver, token);
}
catch(OperationCanceledException){runtime.Log("执行","执行已取消。");}
catch (Exception e) { ShowError(e.Message); }
finally { RunActive = false; StartPlanCommand.NotifyCanExecuteChanged(); CalculateCommand.NotifyCanExecuteChanged(); }
finally { RunActive = false; RefreshTradePlanCommands(); CalculateCommand.NotifyCanExecuteChanged(); }
}
[RelayCommand] private void PauseRun() => runtime.Runner.Pause();
[RelayCommand] private void ResumeRun() { try { runtime.Runner.Resume(); } catch (Exception e) { ShowError(e.Message); } }
@@ -0,0 +1,30 @@
using CommunityToolkit.Mvvm.Input;
using XFEEDToolBox.Core.Presentation;
namespace XFEEDToolBox.ViewModels;
public partial class ToolboxViewModel
{
private bool CanExecuteTradePlan() => SelectedStart is not null && CanCalculate() && !NavigationActive
&& !DemoMode && !Environment.GetCommandLineArgs().Contains("--offline-preview");
partial void OnSelectedStartChanged(StationLiveRow? value) => RefreshTradePlanCommands();
private void RefreshTradePlanCommands()
{
StartPlanCommand.NotifyCanExecuteChanged();
ExecuteTradePlanCommand.NotifyCanExecuteChanged();
}
[RelayCommand(CanExecute = nameof(CanExecuteTradePlan))]
private async Task ExecuteTradePlanAsync(CancellationToken cancellationToken)
{
if (!CanExecuteTradePlan()) return;
ExecutionModeIndex = 2;
Plan = null;
SelectedLeg = null;
await CalculateAsync(cancellationToken);
if (cancellationToken.IsCancellationRequested || HasError || Plan?.Legs.Count is not > 0) return;
await StartPlanAsync(cancellationToken);
}
}
@@ -39,9 +39,8 @@
<NavigationViewItem Content="舰队与装配" add:NavigationAddition.NavigateTo="XFEEDToolBox.Views.ShipPage"/>
<NavigationViewItem Content="模块改装" add:NavigationAddition.NavigateTo="XFEEDToolBox.Views.EngineeringPage"/>
</NavigationViewItem.MenuItems></NavigationViewItem>
<NavigationViewItem Content="贸易与执行" SelectsOnInvoked="False"><NavigationViewItem.Icon><FontIcon Glyph=""/></NavigationViewItem.Icon><NavigationViewItem.MenuItems>
<NavigationViewItem Content="跑商规划" add:NavigationAddition.NavigateTo="XFEEDToolBox.Views.PlannerPage"/>
<NavigationViewItem Content="执行控制" add:NavigationAddition.NavigateTo="XFEEDToolBox.Views.ExecutionPage"/>
<NavigationViewItem Content="贸易" SelectsOnInvoked="False"><NavigationViewItem.Icon><FontIcon Glyph=""/></NavigationViewItem.Icon><NavigationViewItem.MenuItems>
<NavigationViewItem Content="跑商" add:NavigationAddition.NavigateTo="XFEEDToolBox.Views.PlannerPage"/>
</NavigationViewItem.MenuItems></NavigationViewItem>
<NavigationViewItem Content="工具维护" SelectsOnInvoked="False"><NavigationViewItem.Icon><FontIcon Glyph=""/></NavigationViewItem.Icon><NavigationViewItem.MenuItems>
<NavigationViewItem Content="读取验收" add:NavigationAddition.NavigateTo="XFEEDToolBox.Views.ReadPage"/>
@@ -8,7 +8,7 @@
<TextBlock Text="{x:Bind ViewModel.RunMessage, Mode=OneWay}" TextWrapping="Wrap" FontSize="16"/><ProgressBar Value="{x:Bind ViewModel.RunPercent, Mode=OneWay}" Maximum="100"/>
<Grid ColumnSpacing="18"><Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions><StackPanel Spacing="7"><TextBlock Text="下一站" Style="{StaticResource MutedText}"/><TextBlock Text="{x:Bind ViewModel.NextDestination, Mode=OneWay}" TextWrapping="Wrap" FontWeight="SemiBold"/></StackPanel><StackPanel Grid.Column="1" Spacing="7"><TextBlock Text="模拟估计 / 实际交易差额" Style="{StaticResource MutedText}"/><TextBlock Text="{x:Bind ViewModel.ActualProfit, Mode=OneWay}" FontSize="23" Foreground="{StaticResource TealBrush}"/></StackPanel></Grid>
<Border Height="1" Background="{StaticResource PanelStrokeBrush}"/>
<RadioButtons Header="执行方式" SelectedIndex="{x:Bind ViewModel.ExecutionModeIndex,Mode=TwoWay}"><x:String>模拟演练</x:String><x:String>日志领航 · 玩家操作,按事件推进</x:String><RadioButton Content="自动贸易 · 接口校准中" IsEnabled="{x:Bind ViewModel.PluginTradeReady,Mode=OneWay}"/></RadioButtons>
<RadioButtons Header="执行方式" SelectedIndex="{x:Bind ViewModel.ExecutionModeIndex,Mode=TwoWay}"><x:String>模拟演练</x:String><x:String>日志领航 · 玩家操作,按事件推进</x:String><RadioButton Content="自动贸易" IsEnabled="{x:Bind ViewModel.PluginTradeReady,Mode=OneWay}"/></RadioButtons>
<TextBlock Text="{x:Bind ViewModel.ExecutionHint, Mode=OneWay}" Style="{StaticResource MutedText}"/>
<CommandBar Background="Transparent" DefaultLabelPosition="Right"><AppBarButton Icon="Play" Label="开始" controls:AsyncAction.Command="{x:Bind ViewModel.StartPlanCommand}" controls:AsyncAction.CancelCommand="{x:Bind ViewModel.StopRunCommand}"/><AppBarButton Icon="Pause" Label="暂停" Command="{x:Bind ViewModel.PauseRunCommand}"/><AppBarButton Icon="Forward" Label="继续" Command="{x:Bind ViewModel.ResumeRunCommand}"/></CommandBar>
</StackPanel></Border>
@@ -17,7 +17,7 @@
</StackPanel>
<StackPanel Grid.Column="1" Spacing="18">
<Border Style="{StaticResource CardStyle}"><StackPanel Spacing="14"><TextBlock Text="导航助手" Style="{StaticResource SectionTitle}"/><TextBlock Text="复制目的星系后,在游戏星图中规划并确认燃料和航线。工具显示的跳数为估算。" Style="{StaticResource MutedText}"/><Button Content="复制目的星系" Command="{x:Bind ViewModel.CopyDestinationCommand, Mode=OneWay}" HorizontalAlignment="Stretch"/><Button Content="导出路线与货单" Command="{x:Bind ViewModel.ExportPlanCommand, Mode=OneWay}" HorizontalAlignment="Stretch"/></StackPanel></Border>
<Border Style="{StaticResource CardStyle}"><StackPanel Spacing="12"><TextBlock Text="只读验收" Style="{StaticResource SectionTitle}"/><TextBlock Text="Journal 领航从起始站点停靠开始,按实际买卖和靠港事件推进。模拟演练使用路线估计收益。" Style="{StaticResource MutedText}"/><TextBlock Text="当前版本不启动原生挂钩或自动飞行。买卖与导航由玩家在游戏内操作。" Style="{StaticResource MutedText}"/><TextBlock Text="数据读取和路线规划不依赖分辨率、窗口焦点或键鼠绑定。" Foreground="{StaticResource AccentBrush}" TextWrapping="Wrap" FontSize="12"/></StackPanel></Border>
<Border Style="{StaticResource CardStyle}"><StackPanel Spacing="12"><TextBlock Text="只读验收" Style="{StaticResource SectionTitle}"/><TextBlock Text="Journal 领航从起始站点停靠开始,按实际买卖和靠港事件推进。模拟演练使用路线估计收益。" Style="{StaticResource MutedText}"/><TextBlock Text="自动贸易由工具箱执行买卖、航行与站内维护,并核对实际游戏回执。" Style="{StaticResource MutedText}"/><TextBlock Text="数据读取和路线规划不依赖分辨率、窗口焦点或键鼠绑定。" Foreground="{StaticResource AccentBrush}" TextWrapping="Wrap" FontSize="12"/></StackPanel></Border>
<Border Style="{StaticResource CardStyle}"><StackPanel Spacing="12"><TextBlock Text="近期事件" Style="{StaticResource SectionTitle}"/><ItemsControl ItemsSource="{x:Bind ViewModel.Activity, Mode=OneWay}" MaxHeight="330"><ItemsControl.ItemTemplate><DataTemplate x:DataType="live:ActivityLiveRow"><StackPanel Spacing="4" Margin="0,0,0,13"><TextBlock Text="{x:Bind TimeDisplay, Mode=OneWay}" FontSize="10" Foreground="{StaticResource TealBrush}"/><TextBlock Text="{x:Bind Message, Mode=OneWay}" Style="{StaticResource MutedText}" FontSize="12"/></StackPanel></DataTemplate></ItemsControl.ItemTemplate></ItemsControl></StackPanel></Border>
</StackPanel>
</Grid>
@@ -1,6 +1,6 @@
<Page x:Class="XFEEDToolBox.Views.PlannerPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:models="using:XFEEDToolBox.Core.Models" xmlns:controls="using:XFEEDToolBox.Controls">
<Grid RowSpacing="20" Padding="0,8,0,0"><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<Grid><Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions><StackPanel Spacing="6"><TextBlock Text="TRADE ROUTE OPTIMIZER" Style="{StaticResource Eyebrow}"/><TextBlock Text="跑商规划" Style="{StaticResource PageTitle}"/><TextBlock Text="从当前资金出发,比较混合货单、多站航线和往返环线。" Style="{StaticResource MutedText}"/></StackPanel><Button Grid.Column="1" Content="读取玩家参数" Command="{x:Bind ViewModel.UsePlayerValuesCommand}" VerticalAlignment="Center" Padding="18,10"/></Grid>
<Grid><Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions><StackPanel Spacing="6"><TextBlock Text="TRADE PLANNER" Style="{StaticResource Eyebrow}"/><TextBlock Text="跑商" Style="{StaticResource PageTitle}"/><TextBlock Text="从当前资金出发,比较混合货单、多站航线和往返环线。" Style="{StaticResource MutedText}"/></StackPanel><Button Grid.Column="1" Content="读取玩家参数" Command="{x:Bind ViewModel.UsePlayerValuesCommand}" VerticalAlignment="Center" Padding="18,10"/></Grid>
<Grid Grid.Row="1" ColumnSpacing="20"><Grid.ColumnDefinitions><ColumnDefinition Width="310"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<Border Style="{StaticResource CardStyle}" Padding="18,18,12,0"><ScrollViewer VerticalScrollBarVisibility="Auto"><StackPanel Spacing="16" Padding="0,0,6,20">
<TextBlock Text="01 / 舰船与资金" Style="{StaticResource Eyebrow}"/>
@@ -31,9 +31,25 @@
<StackPanel Orientation="Horizontal" Spacing="12"><ProgressRing Width="20" Height="20" IsActive="{x:Bind ViewModel.IsBusy, Mode=OneWay}"/></StackPanel>
</StackPanel></ScrollViewer></Border>
<ScrollViewer Grid.Column="1" VerticalScrollBarVisibility="Auto"><StackPanel Spacing="16" Padding="0,0,8,20">
<Border x:Name="TradeRunCard" Style="{StaticResource CardStyle}" Padding="20">
<StackPanel Spacing="14">
<Grid ColumnSpacing="16"><Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<StackPanel Spacing="6"><TextBlock Text="本次执行" Style="{StaticResource Eyebrow}"/><TextBlock Text="{x:Bind ViewModel.RunStatus, Mode=OneWay}" FontSize="23" FontWeight="SemiBold"/></StackPanel>
<Button x:Name="ExecuteTradePlanButton" Grid.Column="1" Content="自动执行" AutomationProperties.Name="自动执行跑商路线" Style="{StaticResource PrimaryButton}" MinWidth="132" VerticalAlignment="Center" controls:AsyncAction.Command="{x:Bind ViewModel.ExecuteTradePlanCommand}" controls:AsyncAction.CancelCommand="{x:Bind ViewModel.StopRunCommand}"/>
</Grid>
<TextBlock Text="{x:Bind ViewModel.RunMessage, Mode=OneWay}" TextWrapping="Wrap"/>
<ProgressBar Value="{x:Bind ViewModel.RunPercent, Mode=OneWay}" Maximum="100"/>
<Grid ColumnSpacing="20"><Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<StackPanel Spacing="6"><TextBlock Text="下一站" Style="{StaticResource MutedText}"/><TextBlock Text="{x:Bind ViewModel.NextDestination, Mode=OneWay}" FontWeight="SemiBold" TextWrapping="Wrap"/></StackPanel>
<StackPanel Grid.Column="1" Spacing="6"><TextBlock Text="{x:Bind ViewModel.ProfitLabel, Mode=OneWay}" Style="{StaticResource MutedText}"/><TextBlock Text="{x:Bind ViewModel.ActualProfit, Mode=OneWay}" Foreground="{StaticResource TealBrush}" FontSize="21"/><TextBlock Text="{x:Bind ViewModel.ProfitDetails, Mode=OneWay}" Style="{StaticResource MutedText}" FontSize="11"/></StackPanel>
</Grid>
<TextBlock Text="自动完成采购、航行、出售及站内维护,按实际回执更新进度和收益。启动时核对连接、船舶、实时价格及资金。" Style="{StaticResource MutedText}" FontSize="12"/>
</StackPanel>
</Border>
<Grid ColumnSpacing="12"><Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="*"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions><Border Style="{StaticResource CardStyle}" Padding="18"><StackPanel Spacing="10"><TextBlock Text="预计净利润" Style="{StaticResource MutedText}"/><TextBlock Text="{x:Bind ViewModel.PlanProfit, Mode=OneWay}" FontSize="21" Foreground="{StaticResource TealBrush}" FontWeight="SemiBold"/></StackPanel></Border><Border Grid.Column="1" Style="{StaticResource CardStyle}" Padding="18"><StackPanel Spacing="10"><TextBlock Text="预计小时收益" Style="{StaticResource MutedText}"/><TextBlock Text="{x:Bind ViewModel.PlanHourly, Mode=OneWay}" FontSize="21" FontWeight="SemiBold"/></StackPanel></Border><Border Grid.Column="2" Style="{StaticResource CardStyle}" Padding="18"><StackPanel Spacing="10"><TextBlock Text="预计耗时" Style="{StaticResource MutedText}"/><TextBlock Text="{x:Bind ViewModel.PlanDuration, Mode=OneWay}" FontSize="21" FontWeight="SemiBold"/></StackPanel></Border></Grid>
<Border Style="{StaticResource CardStyle}" Padding="0"><Grid><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="245"/></Grid.RowDefinitions><TextBlock Text="{x:Bind ViewModel.PlanInfo, Mode=OneWay}" TextWrapping="Wrap" Margin="20,18,20,0" FontWeight="SemiBold"/><controls:RouteMap Grid.Row="1" Plan="{x:Bind ViewModel.Plan, Mode=OneWay}"/></Grid></Border>
<Grid><Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions><TextBlock Text="逐站货单" Style="{StaticResource SectionTitle}" VerticalAlignment="Center"/><StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8"><Button Content="导出 JSON" Command="{x:Bind ViewModel.ExportPlanCommand}"/><Button Content="进入执行台 →" Click="OpenExecution"/></StackPanel></Grid>
<Grid><Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions><TextBlock Text="逐站货单" Style="{StaticResource SectionTitle}" VerticalAlignment="Center"/><StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8"><Button Content="导出 JSON" Command="{x:Bind ViewModel.ExportPlanCommand}"/></StackPanel></Grid>
<ListView ItemsSource="{x:Bind ViewModel.Plan.Legs, Mode=OneWay}" SelectedItem="{x:Bind ViewModel.SelectedLeg, Mode=TwoWay}" HorizontalContentAlignment="Stretch" MaxHeight="650" SelectionMode="Single"><ListView.ItemTemplate><DataTemplate x:DataType="models:TradeLeg"><Border Background="{StaticResource PanelBrush}" CornerRadius="8" Padding="16" Margin="0,0,0,8"><Grid RowSpacing="10"><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/></Grid.RowDefinitions><Grid><Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions><TextBlock Text="{x:Bind Title}" FontWeight="SemiBold" TextTrimming="CharacterEllipsis"/><TextBlock Grid.Column="1" Text="{x:Bind ProfitDisplay}" Foreground="{StaticResource TealBrush}" Margin="10,0,0,0"/></Grid><TextBlock Grid.Row="1" Text="{x:Bind Systems}" Style="{StaticResource MutedText}"/><TextBlock Grid.Row="2" Text="{x:Bind CargoSummary}" TextWrapping="Wrap" Foreground="{StaticResource AccentBrush}"/><TextBlock Grid.Row="3" Text="{x:Bind Metrics}" Style="{StaticResource MutedText}" FontSize="11"/></Grid></Border></DataTemplate></ListView.ItemTemplate></ListView>
<Border Background="{StaticResource PanelAltBrush}" CornerRadius="8" Padding="18"><StackPanel Spacing="10"><TextBlock Text="计算依据与限制" FontWeight="SemiBold"/><TextBlock Text="{x:Bind ViewModel.PlanWarnings, Mode=OneWay}" Style="{StaticResource MutedText}" FontSize="12"/></StackPanel></Border>
</StackPanel></ScrollViewer>
@@ -4,5 +4,4 @@ public sealed partial class PlannerPage : Page
{
public ToolboxViewModel ViewModel => App.ViewModel;
public PlannerPage() { InitializeComponent(); NavigationCacheMode = Microsoft.UI.Xaml.Navigation.NavigationCacheMode.Required; }
private void OpenExecution(object sender, RoutedEventArgs e) => AppShellPage.Current!.Navigate(typeof(ExecutionPage));
}
@@ -0,0 +1,9 @@
# 跑商页自动执行
路线规划和执行集中在“贸易 → 跑商”。页面显示一个“自动执行”按钮,点击后按当前参数计算路线,核对原生接口与船舶状态,再由工具箱执行原生贸易。无需进入另一个执行台或选择模拟模式;单独计算路线仍可用于预览。
执行期间在同页显示状态、进度、下一站、已确认净利润和实际燃油、维修、补给等支出。原按钮在操作期间提供取消。打开页面只呈现已有数据,不会自动计算、采购或起飞。
界面的停靠交易就绪状态来自只读原生 `describe` 握手,要求正确协议、已核验游戏 SHA-256、停靠服务就绪能力和批量交易能力,避免旧发布清单的 `supportsTrade=false` 错误禁用新接口。实际执行仍需完成原生交易预检,并在采购之前核对 SCO 温控与燃油控制、原始逃逸矢量能力。入口可见不等于这些飞行验收已完成。
本次验证覆盖八项能力检查回归、WinUI Release 构建和后台界面检查;不把界面验证计为一次飞行或交易。