using System.Collections.Concurrent; using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Planning; namespace XFEEDToolBox.Core.Execution; public sealed record AutomaticSupplyRequest(AutomaticTradeConfiguration Configuration,PlayerState Player,decimal Credits,decimal Reserve, IReadOnlyDictionary> ObservedQuotes) {public IReadOnlyList ConfirmedTrades{get;init;}=[];public IReadOnlySet ExcludedSourceMarkets{get;init;}=new HashSet();} public sealed record AutomaticSupplyChoice(Station Source,TradeLeg Delivery,long Supply,long Demand,int FreeCapacity, double EmptyReturnMinutes,double RepositionMinutes,decimal EstimatedNetProfit,double EstimatedMinutes) { public TradeOrder Order=>Delivery.Orders.Single(); public double LoadFactor=>FreeCapacity>0?(double)Order.Quantity/FreeCapacity:0; public long StockFullLoads=>FreeCapacity>0?Supply/FreeCapacity:0; public decimal CreditsPerHour=>EstimatedMinutes>0?EstimatedNetProfit*60/(decimal)EstimatedMinutes:0; public string Basis=>$"按实际供需可装 {Order.Quantity:N0}/{FreeCapacity:N0} t({LoadFactor:P0});该商品库存 {Supply:N0} t。" +$"循环含空载返程 {EmptyReturnMinutes:F1} 分钟、载货交付 {Delivery.EstimatedMinutes:F1} 分钟" +(RepositionMinutes>0?$"及本次转场 {RepositionMinutes:F1} 分钟":"")+";不假定后续补货,到站与每轮交付后重新核对。"; } /// Ranks every compatible CG supply station by a real bounded load and complete repeated-trip time. public static class AutomaticSupplyPlanner { public static MarketBook DeductConfirmedTrades(MarketBook book,IEnumerable receipts) { var rows=book.Quotes.GroupBy(q=>q.MarketId).ToDictionary(g=>g.Key,g=>(IReadOnlyList)g.ToArray()); return book with{Quotes=DeductConfirmedTrades(rows,receipts).Values.SelectMany(q=>q).ToArray()}; } public static MarketBook MergeObservations(MarketBook cache,IReadOnlyDictionary> observations) { var latest=observations.Where(p=>p.Value.Count>0).ToDictionary(p=>p.Key,p=>p.Value.Max(q=>q.CollectedAt)); // A complete observed snapshot masks older cached commodities, // while genuinely newer public quotes can report changed stock. return cache with{Quotes=cache.Quotes.Where(q=>!latest.TryGetValue(q.MarketId,out var at)||q.CollectedAt>at) .Concat(observations.Values.SelectMany(rows=>rows)).ToArray()}; } public static IReadOnlyDictionary> DeductConfirmedTrades( IReadOnlyDictionary> observations,IEnumerable receipts) { var changes=receipts.Where(r=>r.ServerConfirmed&&r.ClientApplied).DistinctBy(r=>r.OperationKey) .GroupBy(r=>(r.MarketId,r.Commodity)).ToDictionary(g=>g.Key,g=>g.ToArray()); return observations.ToDictionary(p=>p.Key,p=>(IReadOnlyList)p.Value.Select(q=> { if(!changes.TryGetValue((q.MarketId,q.Commodity),out var trades))return q; var newer=trades.Where(r=>r.ConfirmedAt>q.CollectedAt).ToArray(); var bought=newer.Where(r=>r.QuantityDelta>0).Sum(r=>(long)r.QuantityDelta); var sold=newer.Where(r=>r.QuantityDelta<0).Sum(r=>-(long)r.QuantityDelta); return q with{Supply=Math.Max(0,q.Supply-bought),Demand=Math.Max(0,q.Demand-sold)}; }).ToArray()); } public static IReadOnlyList Calculate(MarketBook book,PlannerRequest request,Station destination, IReadOnlyCollection acceptedCommodities,int count=8,CancellationToken token=default,IProgress? progress=null) { request.Validate();TradeRoutePlanner.RequireKnownShip(request); if(!TradeSearchConstraints.DestinationAllows(destination,request))return []; if(request.CurrentPosition is not{IsValid:true}||destination.Position is not{IsValid:true})throw new InvalidDataException("CG货源比较需要当前飞船与交付站的真实星系坐标。"); if(!TradeRoutePlanner.IsStationCompatible(destination,request,false))throw new InvalidOperationException("CG交付站不满足实际飞船停机坪或访问条件。"); if(count<1)throw new ArgumentOutOfRangeException(nameof(count)); var allowed=acceptedCommodities.ToHashSet(StringComparer.Ordinal); var commodityFilter=TradeSearchConstraints.NormalizeCommodityFilter(request.CommunityGoalCommodityFilter); if(commodityFilter.Length>0)allowed.IntersectWith(commodityFilter); var center=request.SearchCenterPosition??request.CurrentPosition; // Resolve the newest observation before filtering it. A depleted, // restricted or stale latest quote must never resurrect older stock. var fresh=book.Quotes.GroupBy(q=>(q.MarketId,q.Commodity)) .Select(g=>g.OrderByDescending(q=>q.CollectedAt).ThenBy(q=>q.Supply).First()) .Where(q=>q.CollectedAt<=request.AsOf.AddMinutes(5)&&q.CollectedAt>=request.AsOf.AddMinutes(-request.MaxMarketAgeMinutes)&&!q.Illegal&&!q.Rare&&allowed.Contains(q.Commodity)) .GroupBy(q=>q.MarketId).ToDictionary(g=>g.Key,g=>g.ToArray()); if(!fresh.TryGetValue(destination.MarketId,out var destinationRows))return []; var buyers=destinationRows.Where(q=>q.SellPrice>0&&q.Demand>0&&q.SellPrice<=NativeTradeBatchPolicy.MaximumUnitPrice).ToDictionary(q=>q.Commodity); var sources=book.Stations.Where(s=>s.MarketId!=destination.MarketId&&s.Position is{IsValid:true}&&s.Position.DistanceTo(center)<=request.SearchRadiusLy &&s.Position.DistanceTo(destination.Position)<=request.MaxLegDistanceLy&&TradeRoutePlanner.IsStationCompatible(s,request,false) &&TradeSearchConstraints.SourceAllows(s,request)&&StationSourcePolicy.Allows(s,request.AllowedSourceKinds)&&fresh.ContainsKey(s.MarketId)).DistinctBy(s=>s.MarketId).ToArray(); var found=new ConcurrentBag();long processed=0;var gate=new object(); progress?.Report(new("比较 CG 供货与循环收益",0,sources.Length,"所有合格货源均参与;数量受库存、需求、实际空舱和本金共同约束。")); var budget=Math.Max(0,Math.Min(request.Credits-request.ReserveCredits,request.InvestmentCredits??decimal.MaxValue)); Parallel.ForEach(sources,new ParallelOptions{CancellationToken=token,MaxDegreeOfParallelism=TradeRoutePlanner.MaxParallelism},source=> { var delivery=TradeRoutePlanner.EstimateTravel(source.Position!,source.SystemName,destination,request); var empty=TradeRoutePlanner.EstimateTravel(destination.Position!,destination.SystemName,source,request); var reposition=request.CurrentMarketId==destination.MarketId||request.CurrentMarketId==source.MarketId?null :TradeRoutePlanner.EstimateTravel(request.CurrentPosition!,request.CurrentSystemName,source,request); foreach(var offer in fresh[source.MarketId]) { token.ThrowIfCancellationRequested(); if(offer.BuyPrice is <=0 or >NativeTradeBatchPolicy.MaximumUnitPrice||offer.Supply<=0||!NativeTradeBatchPolicy.AllowsFormal(offer.Commodity) ||!buyers.TryGetValue(offer.Commodity,out var target)||target.SellPrice-offer.BuyPrice(request.Risk switch{RiskTolerance.Conservative=>.4,RiskTolerance.Balanced=>.75,_=>1}))continue; var leg=new TradeLeg{Number=1,From=source,To=destination,Orders=[order],DistanceLy=delivery.Distance,EstimatedJumps=delivery.Jumps, EstimatedMinutes=delivery.Minutes,OperatingCost=delivery.Cost,CashAfter=request.Credits+order.Profit-delivery.Cost,RiskScore=risk}; found.Add(new(source,leg,offer.Supply,target.Demand,request.UsableCargoCapacity,empty.Minutes,reposition?.Minutes??0, profit,delivery.Minutes+empty.Minutes+(reposition?.Minutes??0))); } lock(gate){processed++;progress?.Report(new("比较 CG 供货与循环收益",processed,sources.Length,"已将实际可装载数量及空载返程计入 Cr/h。"));} }); return found.OrderByDescending(c=>c.CreditsPerHour).ThenByDescending(c=>c.EstimatedNetProfit).ThenByDescending(c=>c.Order.Quantity) .ThenBy(c=>c.Source.MarketId).ThenBy(c=>c.Order.Commodity,StringComparer.Ordinal).Take(count).ToArray(); } }