namespace XFEEDToolBox.Core.Galaxy;
public sealed record GalaxyCurrentTradeRoute(long FromSystemAddress,long ToSystemAddress,int OriginalCategoryId,float Amount,
IReadOnlyList<GalaxyCurrentTradeFlow> Contributions);
public sealed record GalaxyCurrentTradeRenderFlow(GalaxyCurrentTradeRoute Route,GalacticSystem From,GalacticSystem To);
public sealed record GalaxyCurrentTradeProjection(IReadOnlyList<GalaxyCurrentTradeRoute> Routes,IReadOnlyList<GalaxyCurrentTradeRenderFlow> Flows,
IReadOnlyList<long> MissingSystemAddresses);
/// <summary>Original current-cache direction, category aggregation and strongest-route selection. No historical fallback.</summary>
public static class GalaxyCurrentTradeRoutes
{
private static readonly Lazy<IReadOnlyDictionary<string,int>> categories=new(()=>GalaxyOriginalTradeCategories.Commodities
.ToDictionary(c=>GalaxyMapFilter.Normalize(c.Name),c=>c.CategoryId,StringComparer.Ordinal));
public static int? CategoryFor(string? originalSymbol)=>originalSymbol is not null&&categories.Value.TryGetValue(GalaxyMapFilter.Normalize(originalSymbol),out var value)?value:null;
public static IReadOnlyList<GalaxyCommodityGroup> Choices(IEnumerable<GalaxyCurrentTradeFlow> flows)
{
return flows.Where(f=>f.CommodityName is not null).DistinctBy(f=>GalaxyMapFilter.Normalize(f.CommodityName!))
.Select(f=>(Flow:f,Category:CategoryFor(f.CommodityName))).GroupBy(f=>f.Category).OrderBy(g=>g.Key??int.MaxValue)
.Select(g=>new GalaxyCommodityGroup(g.Key is {} id?"originaltrade:"+id:"__unknownoriginaltradecategory",
g.Key is {} category?GalaxyOriginalTradeCategories.Categories.Single(c=>c.Id==category).Label:"原贸易分类尚未确认",
g.Select(f=>new GalaxyFilterChoice(GalaxyMapFilter.Normalize(f.Flow.CommodityName!),f.Flow.CommodityName!)).OrderBy(c=>c.Label,StringComparer.Ordinal).ToArray(),g.Key is not null)).ToArray();
}
public static IReadOnlyList<GalaxyCurrentTradeRoute> Select(GalaxyCurrentTradeFlowsSnapshot snapshot,GalaxyTradeFlowSource source,IReadOnlySet<string>? commodities,IReadOnlySet<int>? categories=null)
{
if(!snapshot.Available)return [];
var cutoff=unchecked(snapshot.OriginalClock-snapshot.LifetimeSeconds);
var latest=snapshot.Flows.Where(f=>f.Timestamp>=cutoff&&f.Timestamp<=snapshot.OriginalClock)
.GroupBy(f=>(f.FromSystemAddress,f.ToSystemAddress,f.CommodityToken)).ToDictionary(g=>g.Key,g=>g.OrderByDescending(f=>f.Timestamp).First());
float Amount(GalaxyCurrentTradeFlow row)=>source switch{GalaxyTradeFlowSource.Npc=>row.NpcAmount,GalaxyTradeFlowSource.Players=>row.PlayerAmount,_=>row.NpcAmount+row.PlayerAmount};
var selected=new List<(GalaxyCurrentTradeFlow Flow,int Category)>();
foreach(var row in latest.Values)
{
if(CategoryFor(row.CommodityName) is not {} category||categories is not null&&!categories.Contains(category)||commodities is not null&&!commodities.Contains(GalaxyMapFilter.Normalize(row.CommodityName!)))continue;
var amount=Amount(row);var opposite=latest.TryGetValue((row.ToSystemAddress,row.FromSystemAddress,row.CommodityToken),out var reverse)?Amount(reverse):0;
// 3B92120 uses the positive net amount only as its direction test;
// the per-category route accumulates the original gross amount.
if(float.IsFinite(amount)&&float.IsFinite(opposite)&&amount-opposite>0)selected.Add((row,category));
}
return selected.GroupBy(row=>(row.Flow.FromSystemAddress,row.Flow.ToSystemAddress,row.Category)).Select(group=>
{
float amount=0;foreach(var item in group)amount+=Amount(item.Flow);
return new GalaxyCurrentTradeRoute(group.Key.FromSystemAddress,group.Key.ToSystemAddress,group.Key.Category,amount,group.Select(row=>row.Flow).ToArray());
}).Where(r=>float.IsFinite(r.Amount)&&r.Amount>0).GroupBy(r=>r.FromSystemAddress)
// The original default is four strongest category routes per origin.
// Equal-strength ordering is deterministic here; original hash-table
// tie ordering is not asserted to be identical.
.SelectMany(g=>g.OrderByDescending(r=>r.Amount).ThenBy(r=>r.ToSystemAddress).ThenBy(r=>r.OriginalCategoryId).Take(4))
.OrderBy(r=>r.FromSystemAddress).ThenBy(r=>r.ToSystemAddress).ThenBy(r=>r.OriginalCategoryId).ToArray();
}
public static GalaxyCurrentTradeProjection Project(IReadOnlyList<GalaxyCurrentTradeRoute> routes,IReadOnlyDictionary<long,GalacticSystem> systems)
{
var resolved=new List<GalaxyCurrentTradeRenderFlow>();var missing=new HashSet<long>();
foreach(var route in routes)
{
bool Try(long address,out GalacticSystem? system)
{system=null;if(systems.TryGetValue(address,out var found)&&found.SystemAddress==address&&found.Position.IsValid&&!string.IsNullOrWhiteSpace(found.Name)){system=found;return true;}missing.Add(address);return false;}
var fromKnown=Try(route.FromSystemAddress,out var from);var toKnown=Try(route.ToSystemAddress,out var to);
if(fromKnown&&toKnown)resolved.Add(new(route,from!,to!));
}
return new(routes,resolved,missing.Order().ToArray());
}
}
namespace XFEEDToolBox.Core.Galaxy;
public sealed record GalaxyCurrentTradeRoute(long FromSystemAddress,long ToSystemAddress,int OriginalCategoryId,float Amount,
IReadOnlyList<GalaxyCurrentTradeFlow> Contributions);
public sealed record GalaxyCurrentTradeRenderFlow(GalaxyCurrentTradeRoute Route,GalacticSystem From,GalacticSystem To);
public sealed record GalaxyCurrentTradeProjection(IReadOnlyList<GalaxyCurrentTradeRoute> Routes,IReadOnlyList<GalaxyCurrentTradeRenderFlow> Flows,
IReadOnlyList<long> MissingSystemAddresses);
/// <summary>Original current-cache direction, category aggregation and strongest-route selection. No historical fallback.</summary>
public static class GalaxyCurrentTradeRoutes
{
private static readonly Lazy<IReadOnlyDictionary<string,int>> categories=new(()=>GalaxyOriginalTradeCategories.Commodities
.ToDictionary(c=>GalaxyMapFilter.Normalize(c.Name),c=>c.CategoryId,StringComparer.Ordinal));
public static int? CategoryFor(string? originalSymbol)=>originalSymbol is not null&&categories.Value.TryGetValue(GalaxyMapFilter.Normalize(originalSymbol),out var value)?value:null;
public static IReadOnlyList<GalaxyCommodityGroup> Choices(IEnumerable<GalaxyCurrentTradeFlow> flows)
{
return flows.Where(f=>f.CommodityName is not null).DistinctBy(f=>GalaxyMapFilter.Normalize(f.CommodityName!))
.Select(f=>(Flow:f,Category:CategoryFor(f.CommodityName))).GroupBy(f=>f.Category).OrderBy(g=>g.Key??int.MaxValue)
.Select(g=>new GalaxyCommodityGroup(g.Key is {} id?"originaltrade:"+id:"__unknownoriginaltradecategory",
g.Key is {} category?GalaxyOriginalTradeCategories.Categories.Single(c=>c.Id==category).Label:"原贸易分类尚未确认",
g.Select(f=>new GalaxyFilterChoice(GalaxyMapFilter.Normalize(f.Flow.CommodityName!),f.Flow.CommodityName!)).OrderBy(c=>c.Label,StringComparer.Ordinal).ToArray(),g.Key is not null)).ToArray();
}
public static IReadOnlyList<GalaxyCurrentTradeRoute> Select(GalaxyCurrentTradeFlowsSnapshot snapshot,GalaxyTradeFlowSource source,IReadOnlySet<string>? commodities,IReadOnlySet<int>? categories=null)
{
if(!snapshot.Available)return [];
var cutoff=unchecked(snapshot.OriginalClock-snapshot.LifetimeSeconds);
var latest=snapshot.Flows.Where(f=>f.Timestamp>=cutoff&&f.Timestamp<=snapshot.OriginalClock)
.GroupBy(f=>(f.FromSystemAddress,f.ToSystemAddress,f.CommodityToken)).ToDictionary(g=>g.Key,g=>g.OrderByDescending(f=>f.Timestamp).First());
float Amount(GalaxyCurrentTradeFlow row)=>source switch{GalaxyTradeFlowSource.Npc=>row.NpcAmount,GalaxyTradeFlowSource.Players=>row.PlayerAmount,_=>row.NpcAmount+row.PlayerAmount};
var selected=new List<(GalaxyCurrentTradeFlow Flow,int Category)>();
foreach(var row in latest.Values)
{
if(CategoryFor(row.CommodityName) is not {} category||categories is not null&&!categories.Contains(category)||commodities is not null&&!commodities.Contains(GalaxyMapFilter.Normalize(row.CommodityName!)))continue;
var amount=Amount(row);var opposite=latest.TryGetValue((row.ToSystemAddress,row.FromSystemAddress,row.CommodityToken),out var reverse)?Amount(reverse):0;
// 3B92120 uses the positive net amount only as its direction test;
// the per-category route accumulates the original gross amount.
if(float.IsFinite(amount)&&float.IsFinite(opposite)&&amount-opposite>0)selected.Add((row,category));
}
return selected.GroupBy(row=>(row.Flow.FromSystemAddress,row.Flow.ToSystemAddress,row.Category)).Select(group=>
{
float amount=0;foreach(var item in group)amount+=Amount(item.Flow);
return new GalaxyCurrentTradeRoute(group.Key.FromSystemAddress,group.Key.ToSystemAddress,group.Key.Category,amount,group.Select(row=>row.Flow).ToArray());
}).Where(r=>float.IsFinite(r.Amount)&&r.Amount>0).GroupBy(r=>r.FromSystemAddress)
// The original default is four strongest category routes per origin.
// Equal-strength ordering is deterministic here; original hash-table
// tie ordering is not asserted to be identical.
.SelectMany(g=>g.OrderByDescending(r=>r.Amount).ThenBy(r=>r.ToSystemAddress).ThenBy(r=>r.OriginalCategoryId).Take(4))
.OrderBy(r=>r.FromSystemAddress).ThenBy(r=>r.ToSystemAddress).ThenBy(r=>r.OriginalCategoryId).ToArray();
}
public static GalaxyCurrentTradeProjection Project(IReadOnlyList<GalaxyCurrentTradeRoute> routes,IReadOnlyDictionary<long,GalacticSystem> systems)
{
var resolved=new List<GalaxyCurrentTradeRenderFlow>();var missing=new HashSet<long>();
foreach(var route in routes)
{
bool Try(long address,out GalacticSystem? system)
{system=null;if(systems.TryGetValue(address,out var found)&&found.SystemAddress==address&&found.Position.IsValid&&!string.IsNullOrWhiteSpace(found.Name)){system=found;return true;}missing.Add(address);return false;}
var fromKnown=Try(route.FromSystemAddress,out var from);var toKnown=Try(route.ToSystemAddress,out var to);
if(fromKnown&&toKnown)resolved.Add(new(route,from!,to!));
}
return new(routes,resolved,missing.Order().ToArray());
}
}