using CommunityToolkit.Mvvm.ComponentModel;
using XFEEDToolBox.Core.Galaxy;
using XFEEDToolBox.Core.Navigation;
namespace XFEEDToolBox.ViewModels;
public partial class ToolboxViewModel
{
[ObservableProperty] public partial IReadOnlyList<GalaxyCurrentTradeRenderFlow> GalaxyCurrentTradeFlows { get;set; }=[];
[ObservableProperty] public partial IReadOnlyList<GalaxyCommodityGroup> 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<long,DateTimeOffset> galaxyCurrentTradeAttempts=[];
private IReadOnlyList<GalaxyCurrentTradeFlow>? currentTradeRoutesSource;
private IReadOnlySet<string>? currentTradeRouteSelection;
private IReadOnlySet<int>? currentTradeCategorySelection;
private GalaxyTradeFlowSource currentTradeRouteKind;
private IReadOnlyList<GalaxyCurrentTradeRoute> 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("原当前贸易来源已断开;旧当前流已清除。");
}
}
}
using CommunityToolkit.Mvvm.ComponentModel;
using XFEEDToolBox.Core.Galaxy;
using XFEEDToolBox.Core.Navigation;
namespace XFEEDToolBox.ViewModels;
public partial class ToolboxViewModel
{
[ObservableProperty] public partial IReadOnlyList<GalaxyCurrentTradeRenderFlow> GalaxyCurrentTradeFlows { get;set; }=[];
[ObservableProperty] public partial IReadOnlyList<GalaxyCommodityGroup> 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<long,DateTimeOffset> galaxyCurrentTradeAttempts=[];
private IReadOnlyList<GalaxyCurrentTradeFlow>? currentTradeRoutesSource;
private IReadOnlySet<string>? currentTradeRouteSelection;
private IReadOnlySet<int>? currentTradeCategorySelection;
private GalaxyTradeFlowSource currentTradeRouteKind;
private IReadOnlyList<GalaxyCurrentTradeRoute> 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("原当前贸易来源已断开;旧当前流已清除。");
}
}
}