using CommunityToolkit.Mvvm.ComponentModel; using XFEEDToolBox.Core.Galaxy; using XFEEDToolBox.Core.Navigation; namespace XFEEDToolBox.ViewModels; public partial class ToolboxViewModel { [ObservableProperty] public partial IReadOnlyList GalaxyCurrentTradeFlows { get;set; }=[]; [ObservableProperty] public partial IReadOnlyList GalaxyCurrentTradeChoices { get;set; }=[]; [ObservableProperty] public partial string GalaxyCurrentTradeStatus { get;set; }="原当前贸易流已关闭;不会以历史缓存代替。"; private GalaxyCurrentTradeFlowsSnapshot? galaxyCurrentTradeSnapshot; private string galaxyCurrentTradeContext=""; private string galaxyCurrentTradeRunningContext=""; private CancellationTokenSource? galaxyCurrentTradeStop; private Task? galaxyCurrentTradeTask; private readonly Dictionary galaxyCurrentTradeAttempts=[]; private IReadOnlyList? currentTradeRoutesSource; private IReadOnlySet? currentTradeRouteSelection; private IReadOnlySet? currentTradeCategorySelection; private GalaxyTradeFlowSource currentTradeRouteKind; private IReadOnlyList currentTradeRoutes=[]; private string CurrentTradeContext()=>GalaxyFilter.ShowCurrentTradeFlows&&!string.IsNullOrWhiteSpace(CurrentPlayer.CommanderId)?ThargoidContext():""; private void ClearGalaxyCurrentTrade(string status) { galaxyCurrentTradeSnapshot=null;galaxyCurrentTradeContext="";currentTradeRoutesSource=null;currentTradeRoutes=[]; if(GalaxyCurrentTradeFlows.Count>0)GalaxyCurrentTradeFlows=[]; if(GalaxyCurrentTradeChoices.Count>0)GalaxyCurrentTradeChoices=[]; GalaxyCurrentTradeStatus=status; } private GalaxyCurrentTradeProjection RefreshCurrentTradeProjection() { var snapshot=galaxyCurrentTradeContext==CurrentTradeContext()?galaxyCurrentTradeSnapshot:null; if(snapshot is null||!snapshot.Available)return new([],[],[]); if(!ReferenceEquals(currentTradeRoutesSource,snapshot.Flows)||currentTradeRouteKind!=GalaxyFilter.CurrentTradeSource||!ReferenceEquals(currentTradeRouteSelection,GalaxyFilter.CurrentTradeCommodities)||!ReferenceEquals(currentTradeCategorySelection,GalaxyFilter.CurrentTradeCategories)) { currentTradeRoutesSource=snapshot.Flows;currentTradeRouteKind=GalaxyFilter.CurrentTradeSource;currentTradeRouteSelection=GalaxyFilter.CurrentTradeCommodities;currentTradeCategorySelection=GalaxyFilter.CurrentTradeCategories; currentTradeRoutes=GalaxyCurrentTradeRoutes.Select(snapshot,currentTradeRouteKind,currentTradeRouteSelection,currentTradeCategorySelection); } var required=currentTradeRoutes.SelectMany(r=>new[]{r.FromSystemAddress,r.ToSystemAddress}).ToHashSet(); var systems=galaxyCache.Values.Where(s=>s.SystemAddress is {} id&&required.Contains(id)&&s.Position.IsValid).GroupBy(s=>s.SystemAddress!.Value) .ToDictionary(g=>g.Key,g=>g.OrderByDescending(s=>s.Source=="游戏银河目录").First()); var projection=GalaxyCurrentTradeRoutes.Project(currentTradeRoutes,systems); if(!GalaxyCurrentTradeFlows.SequenceEqual(projection.Flows))GalaxyCurrentTradeFlows=projection.Flows; var unclassified=snapshot.Flows.Count(f=>f.CommodityName is not null&&GalaxyCurrentTradeRoutes.CategoryFor(f.CommodityName) is null); GalaxyCurrentTradeStatus=$"当前缓存 · 已解析 {projection.Flows.Count:N0} / {currentTradeRoutes.Count:N0} 条,{projection.MissingSystemAddresses.Count:N0} 个端点待确认。\n商品名 {snapshot.UnresolvedCount:N0} 条、分类 {unclassified:N0} 条待确认;覆盖非完整。"; return projection; } private void QueueGalaxyCurrentTradeFlows() { var context=CurrentTradeContext(); if(context.Length==0) { galaxyCurrentTradeStop?.Cancel();ClearGalaxyCurrentTrade(GalaxyFilter.ShowCurrentTradeFlows?"原当前贸易缓存未连接;当前流保持未知。":"原当前贸易流已关闭;不会以历史缓存代替。");return; } if(galaxyCurrentTradeRunningContext.Length>0&&galaxyCurrentTradeRunningContext!=context) {galaxyCurrentTradeStop?.Cancel();galaxyCurrentTradeAttempts.Clear();ClearGalaxyCurrentTrade("游戏会话已变化;旧当前贸易流已清除。");} RefreshCurrentTradeProjection(); if(galaxyCurrentTradeTask is{IsCompleted:false}&&galaxyCurrentTradeStop?.IsCancellationRequested==false)return; if(runtime.FlightDriver is not NativeFlightControlDriver{SupportsGalaxyCurrentTradeFlows:true} driver) {ClearGalaxyCurrentTrade("原当前贸易缓存接口尚未连接;当前流保持未知。");return;} galaxyCurrentTradeStop?.Dispose();galaxyCurrentTradeStop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token); galaxyCurrentTradeRunningContext=context; galaxyCurrentTradeTask=RefreshGalaxyCurrentTradeLoopAsync(context,driver.ProcessId,galaxyCurrentTradeStop); } private async Task RefreshGalaxyCurrentTradeLoopAsync(string context,int processId,CancellationTokenSource stop) { var token=stop.Token; bool Own()=>ReferenceEquals(galaxyCurrentTradeStop,stop); bool Latest()=>Own()&&!token.IsCancellationRequested&&CurrentTradeContext()==context&&runtime.FlightDriver is NativeFlightControlDriver{SupportsGalaxyCurrentTradeFlows:true} driver&&driver.ProcessId==processId; try { while(Latest()) { try { var snapshot=await new NativeGalaxyAccountCatalog(processId).ReadCurrentTradeFlowsAsync(token);if(!Latest())return; if(!snapshot.Available)ClearGalaxyCurrentTrade("原当前贸易缓存尚未就绪;不使用历史流替代。"); else { // A clock tick alone must not replace every route and // renderer reference when the actual rows are unchanged. var changed=galaxyCurrentTradeSnapshot is null||!galaxyCurrentTradeSnapshot.Flows.SequenceEqual(snapshot.Flows); if(!changed)snapshot=snapshot with{Flows=galaxyCurrentTradeSnapshot!.Flows}; galaxyCurrentTradeSnapshot=snapshot;galaxyCurrentTradeContext=context; if(changed)GalaxyCurrentTradeChoices=GalaxyCurrentTradeRoutes.Choices(snapshot.Flows); var projection=RefreshCurrentTradeProjection(); if(!NavigationActive&&runtime.FlightDriver is NativeFlightControlDriver{SupportsGalaxyResolveAddress:true}&&NativeGalaxy() is {} catalog) { var now=DateTimeOffset.UtcNow; var pending=projection.MissingSystemAddresses.Where(id=>!galaxyCurrentTradeAttempts.TryGetValue(id,out var last)||now-last>TimeSpan.FromSeconds(30)) .OrderBy(id=>galaxyCurrentTradeAttempts.GetValueOrDefault(id)).Take(8).ToArray(); foreach(var id in pending) { if(!Latest())return;galaxyCurrentTradeAttempts[id]=DateTimeOffset.UtcNow; try{var system=await catalog.ResolveAddressAsync(id,token);if(!Latest())return;RememberGalaxy(system);} catch(Exception error)when(error is KeyNotFoundException or IOException or TimeoutException or InvalidDataException){if(!Latest())return;} } if(!Latest())return;RefreshCurrentTradeProjection(); } } } catch(OperationCanceledException)when(!token.IsCancellationRequested) {if(Latest())ClearGalaxyCurrentTrade("原当前贸易流读取超时;旧当前流已清除。");} catch(Exception error)when(error is IOException or TimeoutException or InvalidDataException or InvalidOperationException or System.Text.Json.JsonException or KeyNotFoundException or FormatException) {if(Latest())ClearGalaxyCurrentTrade("原当前贸易流尚未确认:"+error.Message);} await Task.Delay(2000,token); } } catch(OperationCanceledException){ } finally { // Losing only this capability (same PID/FID) is still a lost // current source. An older cancelled loop must never clear a new // loop's snapshot when it happens to have the same session key. if(Own()&&galaxyCurrentTradeContext==context&&!Latest())ClearGalaxyCurrentTrade("原当前贸易来源已断开;旧当前流已清除。"); } } }