XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Collections.Concurrent;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Execution;
namespace XFEEDToolBox.Core.Planning;

public sealed partial class TradeRoutePlanner
{
    public static int MaxParallelism=>Math.Clamp(Environment.ProcessorCount-1,1,8);
    private readonly record struct CargoCacheKey(long From,long To,decimal Budget,string Remaining);
    private sealed record PreparedNetwork(Station[] Stations,Dictionary<long,Dictionary<string,CommodityQuote>> Fresh,Dictionary<long,List<Edge>> Edges)
    {public ConcurrentDictionary<CargoCacheKey,Lazy<CargoSolution>> CargoCache{get;}=new();}
    private static CargoSolution OptimizePrepared(PreparedNetwork network,Edge edge,IEnumerable<CargoCandidate> available,decimal cash,PlannerRequest request,CancellationToken token)
    {
        var goods=available.ToArray();var budget=Math.Min(cash,goods.Sum(item=>(decimal)item.BuyPrice*item.Limit));
        var key=new CargoCacheKey(edge.From.MarketId,edge.To.MarketId,budget,string.Join("\0",goods.OrderBy(item=>item.Commodity,StringComparer.Ordinal).Select(item=>item.Commodity+":"+item.Limit)));
        if(network.CargoCache.TryGetValue(key,out var previous))return previous.Value;
        // Bound cache memory, not participating stations, edges or searches.
        if(network.CargoCache.Count>=16384)return CargoOptimizer.Optimize(goods,request.UsableCargoCapacity,budget,request.CargoSearchNodes,token);
        return network.CargoCache.GetOrAdd(key,_=>new(()=>CargoOptimizer.Optimize(goods,request.UsableCargoCapacity,budget,request.CargoSearchNodes,token),LazyThreadSafetyMode.ExecutionAndPublication)).Value;
    }
    private sealed record CandidateSearchResult(IReadOnlyList<RoutePlan> Candidates,bool SearchPruned);
    public IReadOnlyList<RoutePlan> CalculateCandidates(MarketBook book,PlannerRequest request,int maxCandidates=8,CancellationToken token=default,IProgress<TradePlanningProgress>? progress=null,IReadOnlyList<CommunityGoalInfo>? communityGoals=null)
        =>CalculateAllCandidates(book,request,maxCandidates,token,progress,request.CommunityGoalSupplyOnly?CommunityGoalSupplyScope.Create(book,communityGoals,request.AsOf,request.CommunityGoalCommodityFilter):null).Candidates;

    public static void RequireKnownShip(PlannerRequest request)
    {
        if(ShipPadPolicy.RequiredPad(request.ActualShipType)==LandingPad.Unknown)
            throw new InvalidOperationException("当前船型尺寸未确认,不能校验路线两端的停机坪;请先读取实际飞船。");
    }

    private static CandidateSearchResult CalculateAllCandidates(MarketBook book,PlannerRequest request,int maxCandidates,CancellationToken token,IProgress<TradePlanningProgress>? progress,CommunityGoalSupplyScope? scope=null)
    {
        request.Validate();RequireKnownShip(request);token.ThrowIfCancellationRequested();
        if(maxCandidates is <1 or >8)throw new ArgumentOutOfRangeException(nameof(maxCandidates));
        if(request.CommunityGoalSupplyOnly&&scope?.Available!=true)
        {progress?.Report(new("CG 目录未确认",0,0,scope?.Issue??"有效活动与接收商品尚未确认;未回退普通跑商。"));return new([],false);}
        if(request.CurrentPosition is not{IsValid:true})throw new InvalidOperationException("飞船当前位置尚未确认,不能估算前往各货源起点的真实转场成本。");
        var selected=book.Stations.FirstOrDefault(station=>station.MarketId==request.StartMarketId);
        var center=request.SearchCenterPosition??request.CurrentPosition??selected?.Position??throw new ArgumentException("搜索中心与真实当前位置均未知。");
        request=request with{Objective=RouteObjective.CreditsPerHour,SearchCenterPosition=center};
        var stations=book.Stations.Where(station=>station.Position is{IsValid:true}&&station.Position.DistanceTo(center)<=request.SearchRadiusLy&&IsStationCompatible(station,request,false))
            .OrderBy(station=>station.MarketId).ToArray();
        var ids=stations.Select(station=>station.MarketId).ToHashSet();
        var fresh=book.Quotes.Where(quote=>ids.Contains(quote.MarketId)&&quote.CollectedAt<=request.AsOf.AddMinutes(5)&&quote.CollectedAt>=request.AsOf.AddMinutes(-request.MaxMarketAgeMinutes)&&!quote.Illegal&&!quote.Rare)
            .GroupBy(quote=>quote.MarketId).ToDictionary(group=>group.Key,group=>group.GroupBy(quote=>quote.Commodity).ToDictionary(goods=>goods.Key,goods=>goods.OrderByDescending(quote=>quote.CollectedAt).ThenBy(quote=>quote.BuyPrice).ThenByDescending(quote=>quote.SellPrice).First()));
        var network=BuildSparseNetwork(stations,fresh,request,token,progress,scope);
        var sources=stations.Where(station=>TradeSearchConstraints.SourceAllows(station,request)&&StationSourcePolicy.Allows(station,request.AllowedSourceKinds)&&fresh.TryGetValue(station.MarketId,out var goods)&&goods.Values.Any(quote=>quote.BuyPrice>0&&quote.Supply>0)).ToArray();
        var found=new Dictionary<string,RoutePlan>(StringComparer.Ordinal);var anyPruned=false;
        IReadOnlyList<RoutePlan> Search(PreparedNetwork graph,string stage,string[]? excluded=null)
        {
            long processed=0;var results=new ConcurrentBag<RoutePlan>();var pruned=0;var progressGate=new object();
            progress?.Report(new(stage,0,sources.Length,"按真实船位置逐一比较全部货源起点"));
            Parallel.ForEach(sources,new ParallelOptions{CancellationToken=token,MaxDegreeOfParallelism=MaxParallelism},source=>
            {
                var input=request with{StartMarketId=source.MarketId};
                var plans=CalculateInternal(book,input,maxCandidates,true,token,graph);
                if(plans.Any(plan=>plan.SearchPruned))Interlocked.Exchange(ref pruned,1);
                foreach(var plan in plans.Where(plan=>plan.Legs.Count>0))results.Add(excluded is null?plan:plan with{IsCargoAlternative=true,ExcludedCommodities=excluded,SearchPruned=true,
                    Warnings=[..plan.Warnings,$"货物备选的有界搜索排除了 {string.Join("、",excluded)};站点输入没有裁剪。"]});
                lock(progressGate){processed++;progress?.Report(new(stage,processed,sources.Length,"已完成该起点的有界路线搜索;不代表枚举所有路线组合"));}
            });
            anyPruned|=pruned!=0;
            var ordered=results.OrderByDescending(plan=>plan.CreditsPerHour).ThenByDescending(plan=>plan.NetProfit).ThenBy(plan=>CandidateKey(plan.Legs),StringComparer.Ordinal).ToArray();
            foreach(var plan in ordered)found.TryAdd(CandidateKey(plan.Legs),plan);return ordered;
        }
        var primary=Search(network,"计算全部货源起点");
        if(primary.Count>0&&found.Count<maxCandidates)
        {
            var pending=new Queue<string[]>();var queued=new HashSet<string>(StringComparer.Ordinal);
            void Queue(IReadOnlyList<RoutePlan> plans,string[] inherited)
            {
                if(plans.Count==0)return;
                foreach(var goods in plans[0].Legs.SelectMany(leg=>leg.Orders).GroupBy(order=>order.Commodity).OrderByDescending(group=>group.Sum(order=>order.Profit)).ThenBy(group=>group.Key,StringComparer.Ordinal).Take(3))
                {var excluded=inherited.Append(goods.Key).Distinct(StringComparer.Ordinal).Order(StringComparer.Ordinal).ToArray();if(queued.Add(string.Join("\0",excluded)))pending.Enqueue(excluded);}
            }
            Queue(primary,[]);
            for(var pass=0;pass<3&&pending.Count>0&&found.Count<maxCandidates;pass++)
            {
                token.ThrowIfCancellationRequested();var excluded=pending.Dequeue();
                var alternative=network with{Edges=network.Edges.ToDictionary(pair=>pair.Key,pair=>pair.Value.Select(edge=>edge with{Candidates=edge.Candidates.Where(item=>!excluded.Contains(item.Commodity,StringComparer.Ordinal)).ToArray()}).Where(edge=>edge.Candidates.Length>0).ToList())};
                Queue(Search(alternative,$"计算货物备选 {pass+1}",excluded),excluded);anyPruned=true;
            }
        }
        token.ThrowIfCancellationRequested();
        var ranked=found.Values.Where(plan=>scope is null||CommunityGoalSupplyScope.MatchesRoute(scope.Goals,plan,request.AsOf))
            .OrderByDescending(plan=>plan.CreditsPerHour).ThenByDescending(plan=>plan.NetProfit).ThenBy(plan=>plan.Minutes).ThenBy(plan=>CandidateKey(plan.Legs),StringComparer.Ordinal).Take(maxCandidates).ToArray();
        progress?.Report(new("完成",sources.Length,sources.Length,$"全部 {stations.Length} 个合规站点参与、{sources.Length} 个货源起点已处理;路线与装载仍使用明确的有界搜索"));
        return new(ranked,anyPruned);
    }

    private static PreparedNetwork BuildSparseNetwork(Station[] stations,Dictionary<long,Dictionary<string,CommodityQuote>> fresh,PlannerRequest request,CancellationToken token,IProgress<TradePlanningProgress>? progress,CommunityGoalSupplyScope? scope)
    {
        var byId=stations.ToDictionary(station=>station.MarketId);var size=request.MaxLegDistanceLy;
        (double X,double Y,double Z) Cell(Coordinates position)=>(Math.Floor(position.X/size),Math.Floor(position.Y/size),Math.Floor(position.Z/size));
        var cells=stations.GroupBy(station=>Cell(station.Position!)).ToDictionary(group=>group.Key,group=>group.ToArray());
        var sales=fresh.SelectMany(station=>station.Value.Values.Where(quote=>quote.SellPrice>0&&quote.Demand>0&&TradeSearchConstraints.DestinationAllows(byId[quote.MarketId],request)&&(scope is null||scope.AllowsSale(quote)))).GroupBy(quote=>quote.Commodity)
            .ToDictionary(group=>group.Key,group=>group.OrderByDescending(quote=>quote.SellPrice).ToArray(),StringComparer.Ordinal);
        var edges=new ConcurrentDictionary<long,List<Edge>>();long processed=0;var progressGate=new object();
        progress?.Report(new("建立商品与空间索引",0,stations.Length,"按商品倒排、价格上界和航段距离剔除不可能成交的配对"));
        Parallel.ForEach(stations,new ParallelOptions{CancellationToken=token,MaxDegreeOfParallelism=MaxParallelism},from=>
        {
            var choices=new List<Edge>();
            if(TradeSearchConstraints.SourceAllows(from,request)&&StationSourcePolicy.Allows(from,request.AllowedSourceKinds)&&fresh.TryGetValue(from.MarketId,out var offers)
                &&offers.Values.Any(quote=>quote.BuyPrice>0&&quote.Supply>0&&sales.TryGetValue(quote.Commodity,out var buyers)&&buyers[0].SellPrice-quote.BuyPrice>=request.MinimumProfitPerTon))
            {
                var cell=Cell(from.Position!);var nearby=new HashSet<long>();
                for(var x=-1;x<=1;x++)for(var y=-1;y<=1;y++)for(var z=-1;z<=1;z++)
                    if(cells.TryGetValue((cell.X+x,cell.Y+y,cell.Z+z),out var bucket))foreach(var to in bucket)
                        if(to.MarketId!=from.MarketId&&from.Position!.DistanceTo(to.Position!)<=request.MaxLegDistanceLy)nearby.Add(to.MarketId);
                var cargo=new Dictionary<long,List<CargoCandidate>>();
                foreach(var offer in offers.Values.Where(quote=>quote.BuyPrice>0&&quote.Supply>0))
                {
                    token.ThrowIfCancellationRequested();if(!sales.TryGetValue(offer.Commodity,out var demands)||demands[0].SellPrice-offer.BuyPrice<request.MinimumProfitPerTon)continue;
                    var matched=nearby.Count<demands.Length
                        ?nearby.Select(id=>fresh.TryGetValue(id,out var rows)?rows.GetValueOrDefault(offer.Commodity):null).Where(quote=>quote is not null&&quote.SellPrice>0&&quote.Demand>0&&quote.SellPrice-offer.BuyPrice>=request.MinimumProfitPerTon&&TradeSearchConstraints.DestinationAllows(byId[quote.MarketId],request)&&(scope is null||scope.AllowsSale(quote))).Select(quote=>quote!)
                        :demands.TakeWhile(quote=>quote.SellPrice-offer.BuyPrice>=request.MinimumProfitPerTon);
                    foreach(var demand in matched)
                    {
                        if(demand.SellPrice-offer.BuyPrice<request.MinimumProfitPerTon)break;
                        if(!nearby.Contains(demand.MarketId))continue;
                        var amount=(int)Math.Min(request.UsableCargoCapacity,Math.Min(offer.Supply,demand.Demand));if(amount<=0)continue;
                        if(!cargo.TryGetValue(demand.MarketId,out var rows))cargo[demand.MarketId]=rows=[];
                        rows.Add(new(offer.Commodity,offer.DisplayName,offer.BuyPrice,demand.SellPrice,amount));
                    }
                }
                foreach(var(toId,goods)in cargo){var to=byId[toId];choices.Add(new(from,to,EstimateTravel(from.Position!,from.SystemName,to,request),Math.Max(SecurityRisk(from.Security),SecurityRisk(to.Security)),goods.ToArray()));}
            }
            edges[from.MarketId]=choices;lock(progressGate){processed++;progress?.Report(new("建立商品与空间索引",processed,stations.Length,"只生成距离内且存在正利润商品的连接"));}
        });
        return new(stations,fresh,edges.ToDictionary(pair=>pair.Key,pair=>pair.Value));
    }
}