namespace XFEEDToolBox.Core.Models;
public enum DataSource { Demonstration, Capture, Spansh, Eddn, Journal }
public enum SecurityLevel { Unknown, High, Medium, Low, Anarchy }
public enum LandingPad { Unknown, Small, Medium, Large }
public enum RiskTolerance { Conservative, Balanced, Adventurous }
public enum RouteObjective { CreditsPerHour, TotalProfit, ReturnOnCapital }
public enum TradeRiskTier { Low, Moderate, High, VeryHigh, Unknown }
public enum ExecutionMode { Simulation, JournalGuide, NativeBridge }
public sealed record Coordinates(double X, double Y, double Z)
{
public double DistanceTo(Coordinates other) => Math.Sqrt(
Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2) + Math.Pow(Z - other.Z, 2));
public bool IsValid => double.IsFinite(X) && double.IsFinite(Y) && double.IsFinite(Z);
}
public sealed record Station
{
public long MarketId { get; init; }
public string Name { get; init; } = "";
public string SystemName { get; init; } = "";
public Coordinates? Position { get; init; }
public double? DistanceToArrivalLs { get; init; }
public SecurityLevel Security { get; init; }
public LandingPad MaxLandingPad { get; init; }
public string Type { get; init; } = "";
public bool IsPlanetary { get; init; }
public bool IsFleetCarrier { get; init; }
public bool RequiresPermit { get; init; }
public bool IsRestricted { get; init; }
public DateTimeOffset UpdatedAt { get; init; }
public string DisplayName => $"{SystemName} / {Name}";
public string PadDisplay => MaxLandingPad switch { LandingPad.Large => "L", LandingPad.Medium => "M", LandingPad.Small => "S", _ => "未知" };
public string SecurityDisplay => Security switch { SecurityLevel.High => "高安", SecurityLevel.Medium => "中安", SecurityLevel.Low => "低安", SecurityLevel.Anarchy => "无政府", _ => "安保未知" };
}
public sealed record CommodityQuote
{
public long MarketId { get; init; }
public string Commodity { get; init; } = "";
public string DisplayName { get; init; } = "";
public string Category { get; init; } = "";
public long BuyPrice { get; init; }
public long SellPrice { get; init; }
public long MeanPrice { get; init; }
public long Supply { get; init; }
public long Demand { get; init; }
public bool Illegal { get; init; }
public bool Rare { get; init; }
public DateTimeOffset CollectedAt { get; init; }
public DataSource Source { get; init; }
public string SourceDisplay => Source switch { DataSource.Journal => "本机 Market", DataSource.Eddn => "EDDN", DataSource.Spansh => "Spansh", DataSource.Capture => "历史抓包", _ => "演示数据" };
public string AgeDisplay => FormatAge(DateTimeOffset.UtcNow - CollectedAt);
public static string FormatAge(TimeSpan age) => age.TotalMinutes < 1 ? "刚刚" : age.TotalHours < 1 ? $"{(int)age.TotalMinutes} 分钟前" : age.TotalDays < 1 ? $"{age.TotalHours:F1} 小时前" : $"{age.TotalDays:F1} 天前";
}
public sealed record MarketSnapshot(Station Station, IReadOnlyList<CommodityQuote> Quotes, DateTimeOffset CollectedAt, DataSource Source);
public sealed record MarketBook(IReadOnlyList<Station> Stations, IReadOnlyList<CommodityQuote> Quotes, bool Truncated = false);
public sealed record MarketRow(Station Station, CommodityQuote Quote)
{
public string Name => Quote.DisplayName;
public string Location => Station.DisplayName;
public string Buy => Quote.BuyPrice > 0 ? Quote.BuyPrice.ToString("N0") : "—";
public string Sell => Quote.SellPrice > 0 ? Quote.SellPrice.ToString("N0") : "—";
public string Supply => Quote.Supply.ToString("N0");
public string Demand => Quote.Demand.ToString("N0");
public string Updated => Quote.AgeDisplay;
public string Source => Quote.SourceDisplay;
}
public sealed record InventoryItem(string Key, string Name, int Count, int Stolen = 0, string Group = "货舱", long? MissionId = null);
public sealed record PlayerState
{
public string Commander { get; init; } = "尚未读取指挥官";
public string CommanderId { get; init; } = "";
public string Ship { get; init; } = "";
public string GameVersion { get; init; } = "";
public string GameBuild { get; init; } = "";
public string GameMode { get; init; } = "";
public string GroupName { get; init; } = "";
public DateTimeOffset? LoadGameAt { get; init; }
public bool AdvancedDocking { get; init; }
public bool SupercruiseAssist { get; init; }
public decimal? Credits { get; init; }
public int CargoCapacity { get; init; }
public int CargoCount { get; init; }
public double? MaxJumpRange { get; init; }
public decimal Rebuy { get; init; }
public string SystemName { get; init; } = "";
public string StationName { get; init; } = "";
public long? MarketId { get; init; }
public IReadOnlyList<string> StationServices { get; init; }=[];
public bool StationServicesKnown { get; init; }
public Coordinates? Position { get; init; }
public bool Docked { get; init; }
public bool InDanger { get; init; }
public bool InGame { get; init; }
public bool CreditsReliable { get; init; }
public DateTimeOffset UpdatedAt { get; init; }
public DateTimeOffset? StatusAt { get; init; }
// Successful local file reads are a separate clock from the game's last change.
public DateTimeOffset? StatusReadAt { get; init; }
public DateTimeOffset? StatusWrittenAt { get; init; }
public DateTimeOffset? CreditsAt { get; init; }
public DateTimeOffset? CargoAt { get; init; }
public DateTimeOffset? MaterialsAt { get; init; }
public DateTimeOffset? LockerAt { get; init; }
public DateTimeOffset? BackpackAt { get; init; }
public DateTimeOffset? LocationAt { get; init; }
public bool CargoReliable { get; init; }
public bool MaterialsReliable { get; init; }
public bool LockerReliable { get; init; }
public bool BackpackReliable { get; init; }
public ShipStatus? Status { get; init; }
public CommanderProfile Profile{get;init;}=new();
public IReadOnlyList<InventoryItem> Inventory { get; init; } = [];
public IReadOnlyList<InventoryItem> Materials { get; init; } = [];
public IReadOnlyList<XFEEDToolBox.Core.Engineering.EngineerAccess> Engineers { get; init; } = [];
public DateTimeOffset? EngineersAt { get; init; }
public int VerifiedEngineeringRolls { get; init; }
public int MismatchedEngineeringRolls { get; init; }
public IReadOnlyList<InventoryItem> Locker { get; init; } = [];
public IReadOnlyList<InventoryItem> Backpack { get; init; } = [];
public long? CurrentShipId { get; init; }
public IReadOnlyList<ShipRecord> Ships { get; init; } = [];
public IReadOnlyList<StoredModule> StoredModules { get; init; } = [];
public DateTimeOffset? FleetAt { get; init; }
public DateTimeOffset? StoredModulesAt { get; init; }
public XFEEDToolBox.Core.Navigation.NavigationJournalState Navigation { get; init; }=new();
public int FreeCargo => Math.Max(0, CargoCapacity - CargoCount);
}
public sealed record TradeOrder(string Commodity, string Name, int Quantity, long BuyPrice, long SellPrice)
{
public decimal Cost => (decimal)Quantity * BuyPrice;
public decimal Revenue => (decimal)Quantity * SellPrice;
public decimal Profit => Revenue - Cost;
public string Summary => $"{Name} × {Quantity:N0} t";
public string Prices => $"{BuyPrice:N0} → {SellPrice:N0} Cr/t";
}
public sealed record TradeLeg
{
public int Number { get; init; }
public Station From { get; init; } = new();
public Station To { get; init; } = new();
public IReadOnlyList<TradeOrder> Orders { get; init; } = [];
public double DistanceLy { get; init; }
public int EstimatedJumps { get; init; }
public double EstimatedMinutes { get; init; }
public decimal OperatingCost { get; init; }
public double RiskScore { get; init; }
public decimal CashAfter { get; init; }
public bool CargoOptimal { get; init; }
public TradeRiskTier RiskTier => From.Security == SecurityLevel.Unknown || To.Security == SecurityLevel.Unknown
? TradeRiskTier.Unknown : RiskScore < .3 ? TradeRiskTier.Low : RiskScore < .6 ? TradeRiskTier.Moderate : RiskScore < .8 ? TradeRiskTier.High : TradeRiskTier.VeryHigh;
public string RiskDisplay => RiskTier switch { TradeRiskTier.Low => "低", TradeRiskTier.Moderate => "中", TradeRiskTier.High => "高", TradeRiskTier.VeryHigh => "很高", _ => "信息不足" };
public decimal GrossProfit => Orders.Sum(x => x.Profit);
public decimal NetProfit => GrossProfit - OperatingCost;
public int Quantity => Orders.Sum(x => x.Quantity);
public string Title => $"{Number:00} {From.Name} → {To.Name}";
public string Systems => $"{From.SystemName} → {To.SystemName}";
public string CargoSummary => Orders.Count == 0 ? "空载转场" : string.Join(" · ", Orders.Select(x => x.Summary));
public string Metrics => $"{DistanceLy:F1} ly · 约 {EstimatedJumps} 跳 · {EstimatedMinutes:F1} 分钟 · 风险等级 {RiskDisplay}";
public string ProfitDisplay => $"+{NetProfit:N0} Cr";
}
public sealed record RoutePlan
{
public Guid Id { get; init; } = Guid.NewGuid();
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
public PlannerRequest Request { get; init; } = new();
public IReadOnlyList<TradeLeg> Legs { get; init; } = [];
public IReadOnlyList<string> Warnings { get; init; } = [];
public int StationsConsidered { get; init; }
public int StatesExplored { get; init; }
public double DeadheadDistanceLy { get; init; }
public int DeadheadJumps { get; init; }
public double DeadheadMinutes { get; init; }
public decimal DeadheadOperatingCost { get; init; }
public bool SearchPruned { get; init; }
public bool IsCargoAlternative { get; init; }
public IReadOnlyList<string> ExcludedCommodities { get; init; } = [];
public bool SearchBounded => true;
public bool IsEstimate => true;
public bool CargoOptimal => Legs.All(x => x.CargoOptimal);
public int EstimatedTradingLegs => Legs.Count(x => x.Orders.Count > 0);
public TradeRiskTier RiskTier => Legs.Count == 0 || Legs.Any(x => x.RiskTier == TradeRiskTier.Unknown)
? TradeRiskTier.Unknown : Legs.Max(x => x.RiskTier);
public string RiskDisplay => RiskTier switch { TradeRiskTier.Low => "低", TradeRiskTier.Moderate => "中", TradeRiskTier.High => "高", TradeRiskTier.VeryHigh => "很高", _ => "信息不足" };
public string RiskMethod => "依据安保、行情时效和成交量占需求比评估;风险等级不是概率。";
public decimal NetProfit => Legs.Sum(x => x.NetProfit) - DeadheadOperatingCost;
public double Minutes => Legs.Sum(x => x.EstimatedMinutes) + DeadheadMinutes;
public decimal CreditsPerHour => Minutes > 0 ? NetProfit * 60 / (decimal)Minutes : 0;
public bool IsLoop => Legs.Count > 1 && Legs[0].From.MarketId == Legs[^1].To.MarketId;
public string Method => "限域束搜索 + 有界混载分支定界";
}
public sealed record PlannerRequest
{
public long StartMarketId { get; init; }
public decimal Credits { get; init; }
public decimal ReserveCredits { get; init; }
public decimal? InvestmentCredits { get; init; }
public int CargoCapacity { get; init; } = 64;
public int OccupiedCargo { get; init; }
public int UsableCargoCapacity => Math.Max(0, CargoCapacity - OccupiedCargo);
public double LadenJumpRangeLy { get; init; } = 20;
public bool JumpRangeIsEstimate { get; init; }
public double EstimatedJumpRangeFactor { get; init; } = .8;
public double EffectiveLadenJumpRangeLy => LadenJumpRangeLy * (JumpRangeIsEstimate ? EstimatedJumpRangeFactor : 1);
public Coordinates? CurrentPosition { get; init; }
public string CurrentSystemName { get; init; } = "";
public long? CurrentMarketId { get; init; }
public double MaxLegDistanceLy { get; init; } = 60;
public double SearchRadiusLy { get; init; } = 500;
public Coordinates? SearchCenterPosition {get;init;}
public string SearchCenterName {get;init;}="";
public int MaxLegs { get; init; } = 4;
public bool Loop { get; init; } = true;
public bool CommunityGoalSupplyOnly { get; init; }
public string[] CommunityGoalCommodityFilter {get;init;}=[];
public long? SourceMarketIdFilter{get;init;}
public long? DestinationMarketIdFilter{get;init;}
public string SourceSystemFilter{get;init;}="";
public string SourceStationFilter{get;init;}="";
public string DestinationSystemFilter{get;init;}="";
public string DestinationStationFilter{get;init;}="";
public RouteObjective Objective { get; init; } = RouteObjective.CreditsPerHour;
public RiskTolerance Risk { get; init; } = RiskTolerance.Balanced;
public LandingPad RequiredPad { get; init; } = LandingPad.Large;
public string ActualShipType {get;init;}="";
public LandingPad ShipRequiredPad {get;init;}=LandingPad.Unknown;
public LandingPad EffectiveRequiredPad=>Planning.ShipPadPolicy.EffectiveRequiredPad(RequiredPad,ShipRequiredPad,ActualShipType);
public StationSourceKinds AllowedSourceKinds {get;init;}=StationSourceKinds.AllKnown;
public bool IncludePlanetary { get; init; }
public bool IncludeCarriers { get; init; }
public bool AllowUnknownMetadata { get; init; }
public int MaxMarketAgeMinutes { get; init; } = 360;
public double MaxArrivalDistanceLs { get; init; } = 10000;
public double MinutesPerJump { get; init; } = 1;
public double DockingMinutes { get; init; } = 4;
public decimal CostPerJump { get; init; } = 100;
public decimal CostPerMinute { get; init; }
public decimal MinimumProfitPerTon { get; init; } = 100;
public int BeamWidth { get; init; } = 48;
public int CargoSearchNodes { get; init; } = 2500;
public DateTimeOffset AsOf { get; init; } = DateTimeOffset.UtcNow;
public void Validate()
{
if(SourceMarketIdFilter is <=0||DestinationMarketIdFilter is <=0)throw new ArgumentException("明确指定的站点身份须有效;不指定时请留空。");
if (Credits <= ReserveCredits || ReserveCredits < 0) throw new ArgumentException("可用现金必须高于保留资金。");
if (InvestmentCredits is <= 0) throw new ArgumentException("固定投入金额必须大于零;使用全部可用资金时应留空。");
if (CargoCapacity < 1) throw new ArgumentException("实际货舱容量须大于零。");
if (OccupiedCargo < 0 || OccupiedCargo >= CargoCapacity) throw new ArgumentException("现有货物必须少于货舱总容量;请先腾出可用货舱。");
if (!double.IsFinite(LadenJumpRangeLy) || LadenJumpRangeLy is <= 0 or > 500) throw new ArgumentException("请填写有效的满载跳跃距离。");
if (!double.IsFinite(EstimatedJumpRangeFactor) || EstimatedJumpRangeFactor is <= 0 or > 1) throw new ArgumentException("估算满载跳距的保守系数须大于零且不超过 1。");
if (CurrentPosition is { IsValid: false }) throw new ArgumentException("当前位置坐标无效,无法估算前往起始站点的时间。");
if(SearchCenterPosition is{IsValid:false})throw new ArgumentException("搜索中心坐标无效。");
if (!double.IsFinite(MaxLegDistanceLy) || MaxLegDistanceLy <= 0 || !double.IsFinite(SearchRadiusLy) || SearchRadiusLy <= 0) throw new ArgumentException("搜索半径与航段距离必须大于零。");
if (MaxLegs is < 1 or > 8 || Loop && MaxLegs < 2) throw new ArgumentException("航段数量须在 1–8 之间,环线至少需要 2 段。");
if (MaxMarketAgeMinutes is < 1 or > 10080) throw new ArgumentException("行情时效须在 1 分钟至 7 天内。");
if (CostPerJump < 0 || CostPerMinute < 0 || MinimumProfitPerTon < 0 || !double.IsFinite(MinutesPerJump) || MinutesPerJump <= 0 || !double.IsFinite(DockingMinutes) || DockingMinutes <= 0 || !double.IsFinite(MaxArrivalDistanceLs) || MaxArrivalDistanceLs <= 0) throw new ArgumentException("时间、费用与距离参数无效。");
if (BeamWidth is < 1 or > 128 || CargoSearchNodes is < 1 or > 20000) throw new ArgumentException("搜索预算超出范围。");
}
}
namespace XFEEDToolBox.Core.Models;
public enum DataSource { Demonstration, Capture, Spansh, Eddn, Journal }
public enum SecurityLevel { Unknown, High, Medium, Low, Anarchy }
public enum LandingPad { Unknown, Small, Medium, Large }
public enum RiskTolerance { Conservative, Balanced, Adventurous }
public enum RouteObjective { CreditsPerHour, TotalProfit, ReturnOnCapital }
public enum TradeRiskTier { Low, Moderate, High, VeryHigh, Unknown }
public enum ExecutionMode { Simulation, JournalGuide, NativeBridge }
public sealed record Coordinates(double X, double Y, double Z)
{
public double DistanceTo(Coordinates other) => Math.Sqrt(
Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2) + Math.Pow(Z - other.Z, 2));
public bool IsValid => double.IsFinite(X) && double.IsFinite(Y) && double.IsFinite(Z);
}
public sealed record Station
{
public long MarketId { get; init; }
public string Name { get; init; } = "";
public string SystemName { get; init; } = "";
public Coordinates? Position { get; init; }
public double? DistanceToArrivalLs { get; init; }
public SecurityLevel Security { get; init; }
public LandingPad MaxLandingPad { get; init; }
public string Type { get; init; } = "";
public bool IsPlanetary { get; init; }
public bool IsFleetCarrier { get; init; }
public bool RequiresPermit { get; init; }
public bool IsRestricted { get; init; }
public DateTimeOffset UpdatedAt { get; init; }
public string DisplayName => $"{SystemName} / {Name}";
public string PadDisplay => MaxLandingPad switch { LandingPad.Large => "L", LandingPad.Medium => "M", LandingPad.Small => "S", _ => "未知" };
public string SecurityDisplay => Security switch { SecurityLevel.High => "高安", SecurityLevel.Medium => "中安", SecurityLevel.Low => "低安", SecurityLevel.Anarchy => "无政府", _ => "安保未知" };
}
public sealed record CommodityQuote
{
public long MarketId { get; init; }
public string Commodity { get; init; } = "";
public string DisplayName { get; init; } = "";
public string Category { get; init; } = "";
public long BuyPrice { get; init; }
public long SellPrice { get; init; }
public long MeanPrice { get; init; }
public long Supply { get; init; }
public long Demand { get; init; }
public bool Illegal { get; init; }
public bool Rare { get; init; }
public DateTimeOffset CollectedAt { get; init; }
public DataSource Source { get; init; }
public string SourceDisplay => Source switch { DataSource.Journal => "本机 Market", DataSource.Eddn => "EDDN", DataSource.Spansh => "Spansh", DataSource.Capture => "历史抓包", _ => "演示数据" };
public string AgeDisplay => FormatAge(DateTimeOffset.UtcNow - CollectedAt);
public static string FormatAge(TimeSpan age) => age.TotalMinutes < 1 ? "刚刚" : age.TotalHours < 1 ? $"{(int)age.TotalMinutes} 分钟前" : age.TotalDays < 1 ? $"{age.TotalHours:F1} 小时前" : $"{age.TotalDays:F1} 天前";
}
public sealed record MarketSnapshot(Station Station, IReadOnlyList<CommodityQuote> Quotes, DateTimeOffset CollectedAt, DataSource Source);
public sealed record MarketBook(IReadOnlyList<Station> Stations, IReadOnlyList<CommodityQuote> Quotes, bool Truncated = false);
public sealed record MarketRow(Station Station, CommodityQuote Quote)
{
public string Name => Quote.DisplayName;
public string Location => Station.DisplayName;
public string Buy => Quote.BuyPrice > 0 ? Quote.BuyPrice.ToString("N0") : "—";
public string Sell => Quote.SellPrice > 0 ? Quote.SellPrice.ToString("N0") : "—";
public string Supply => Quote.Supply.ToString("N0");
public string Demand => Quote.Demand.ToString("N0");
public string Updated => Quote.AgeDisplay;
public string Source => Quote.SourceDisplay;
}
public sealed record InventoryItem(string Key, string Name, int Count, int Stolen = 0, string Group = "货舱", long? MissionId = null);
public sealed record PlayerState
{
public string Commander { get; init; } = "尚未读取指挥官";
public string CommanderId { get; init; } = "";
public string Ship { get; init; } = "";
public string GameVersion { get; init; } = "";
public string GameBuild { get; init; } = "";
public string GameMode { get; init; } = "";
public string GroupName { get; init; } = "";
public DateTimeOffset? LoadGameAt { get; init; }
public bool AdvancedDocking { get; init; }
public bool SupercruiseAssist { get; init; }
public decimal? Credits { get; init; }
public int CargoCapacity { get; init; }
public int CargoCount { get; init; }
public double? MaxJumpRange { get; init; }
public decimal Rebuy { get; init; }
public string SystemName { get; init; } = "";
public string StationName { get; init; } = "";
public long? MarketId { get; init; }
public IReadOnlyList<string> StationServices { get; init; }=[];
public bool StationServicesKnown { get; init; }
public Coordinates? Position { get; init; }
public bool Docked { get; init; }
public bool InDanger { get; init; }
public bool InGame { get; init; }
public bool CreditsReliable { get; init; }
public DateTimeOffset UpdatedAt { get; init; }
public DateTimeOffset? StatusAt { get; init; }
// Successful local file reads are a separate clock from the game's last change.
public DateTimeOffset? StatusReadAt { get; init; }
public DateTimeOffset? StatusWrittenAt { get; init; }
public DateTimeOffset? CreditsAt { get; init; }
public DateTimeOffset? CargoAt { get; init; }
public DateTimeOffset? MaterialsAt { get; init; }
public DateTimeOffset? LockerAt { get; init; }
public DateTimeOffset? BackpackAt { get; init; }
public DateTimeOffset? LocationAt { get; init; }
public bool CargoReliable { get; init; }
public bool MaterialsReliable { get; init; }
public bool LockerReliable { get; init; }
public bool BackpackReliable { get; init; }
public ShipStatus? Status { get; init; }
public CommanderProfile Profile{get;init;}=new();
public IReadOnlyList<InventoryItem> Inventory { get; init; } = [];
public IReadOnlyList<InventoryItem> Materials { get; init; } = [];
public IReadOnlyList<XFEEDToolBox.Core.Engineering.EngineerAccess> Engineers { get; init; } = [];
public DateTimeOffset? EngineersAt { get; init; }
public int VerifiedEngineeringRolls { get; init; }
public int MismatchedEngineeringRolls { get; init; }
public IReadOnlyList<InventoryItem> Locker { get; init; } = [];
public IReadOnlyList<InventoryItem> Backpack { get; init; } = [];
public long? CurrentShipId { get; init; }
public IReadOnlyList<ShipRecord> Ships { get; init; } = [];
public IReadOnlyList<StoredModule> StoredModules { get; init; } = [];
public DateTimeOffset? FleetAt { get; init; }
public DateTimeOffset? StoredModulesAt { get; init; }
public XFEEDToolBox.Core.Navigation.NavigationJournalState Navigation { get; init; }=new();
public int FreeCargo => Math.Max(0, CargoCapacity - CargoCount);
}
public sealed record TradeOrder(string Commodity, string Name, int Quantity, long BuyPrice, long SellPrice)
{
public decimal Cost => (decimal)Quantity * BuyPrice;
public decimal Revenue => (decimal)Quantity * SellPrice;
public decimal Profit => Revenue - Cost;
public string Summary => $"{Name} × {Quantity:N0} t";
public string Prices => $"{BuyPrice:N0} → {SellPrice:N0} Cr/t";
}
public sealed record TradeLeg
{
public int Number { get; init; }
public Station From { get; init; } = new();
public Station To { get; init; } = new();
public IReadOnlyList<TradeOrder> Orders { get; init; } = [];
public double DistanceLy { get; init; }
public int EstimatedJumps { get; init; }
public double EstimatedMinutes { get; init; }
public decimal OperatingCost { get; init; }
public double RiskScore { get; init; }
public decimal CashAfter { get; init; }
public bool CargoOptimal { get; init; }
public TradeRiskTier RiskTier => From.Security == SecurityLevel.Unknown || To.Security == SecurityLevel.Unknown
? TradeRiskTier.Unknown : RiskScore < .3 ? TradeRiskTier.Low : RiskScore < .6 ? TradeRiskTier.Moderate : RiskScore < .8 ? TradeRiskTier.High : TradeRiskTier.VeryHigh;
public string RiskDisplay => RiskTier switch { TradeRiskTier.Low => "低", TradeRiskTier.Moderate => "中", TradeRiskTier.High => "高", TradeRiskTier.VeryHigh => "很高", _ => "信息不足" };
public decimal GrossProfit => Orders.Sum(x => x.Profit);
public decimal NetProfit => GrossProfit - OperatingCost;
public int Quantity => Orders.Sum(x => x.Quantity);
public string Title => $"{Number:00} {From.Name} → {To.Name}";
public string Systems => $"{From.SystemName} → {To.SystemName}";
public string CargoSummary => Orders.Count == 0 ? "空载转场" : string.Join(" · ", Orders.Select(x => x.Summary));
public string Metrics => $"{DistanceLy:F1} ly · 约 {EstimatedJumps} 跳 · {EstimatedMinutes:F1} 分钟 · 风险等级 {RiskDisplay}";
public string ProfitDisplay => $"+{NetProfit:N0} Cr";
}
public sealed record RoutePlan
{
public Guid Id { get; init; } = Guid.NewGuid();
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
public PlannerRequest Request { get; init; } = new();
public IReadOnlyList<TradeLeg> Legs { get; init; } = [];
public IReadOnlyList<string> Warnings { get; init; } = [];
public int StationsConsidered { get; init; }
public int StatesExplored { get; init; }
public double DeadheadDistanceLy { get; init; }
public int DeadheadJumps { get; init; }
public double DeadheadMinutes { get; init; }
public decimal DeadheadOperatingCost { get; init; }
public bool SearchPruned { get; init; }
public bool IsCargoAlternative { get; init; }
public IReadOnlyList<string> ExcludedCommodities { get; init; } = [];
public bool SearchBounded => true;
public bool IsEstimate => true;
public bool CargoOptimal => Legs.All(x => x.CargoOptimal);
public int EstimatedTradingLegs => Legs.Count(x => x.Orders.Count > 0);
public TradeRiskTier RiskTier => Legs.Count == 0 || Legs.Any(x => x.RiskTier == TradeRiskTier.Unknown)
? TradeRiskTier.Unknown : Legs.Max(x => x.RiskTier);
public string RiskDisplay => RiskTier switch { TradeRiskTier.Low => "低", TradeRiskTier.Moderate => "中", TradeRiskTier.High => "高", TradeRiskTier.VeryHigh => "很高", _ => "信息不足" };
public string RiskMethod => "依据安保、行情时效和成交量占需求比评估;风险等级不是概率。";
public decimal NetProfit => Legs.Sum(x => x.NetProfit) - DeadheadOperatingCost;
public double Minutes => Legs.Sum(x => x.EstimatedMinutes) + DeadheadMinutes;
public decimal CreditsPerHour => Minutes > 0 ? NetProfit * 60 / (decimal)Minutes : 0;
public bool IsLoop => Legs.Count > 1 && Legs[0].From.MarketId == Legs[^1].To.MarketId;
public string Method => "限域束搜索 + 有界混载分支定界";
}
public sealed record PlannerRequest
{
public long StartMarketId { get; init; }
public decimal Credits { get; init; }
public decimal ReserveCredits { get; init; }
public decimal? InvestmentCredits { get; init; }
public int CargoCapacity { get; init; } = 64;
public int OccupiedCargo { get; init; }
public int UsableCargoCapacity => Math.Max(0, CargoCapacity - OccupiedCargo);
public double LadenJumpRangeLy { get; init; } = 20;
public bool JumpRangeIsEstimate { get; init; }
public double EstimatedJumpRangeFactor { get; init; } = .8;
public double EffectiveLadenJumpRangeLy => LadenJumpRangeLy * (JumpRangeIsEstimate ? EstimatedJumpRangeFactor : 1);
public Coordinates? CurrentPosition { get; init; }
public string CurrentSystemName { get; init; } = "";
public long? CurrentMarketId { get; init; }
public double MaxLegDistanceLy { get; init; } = 60;
public double SearchRadiusLy { get; init; } = 500;
public Coordinates? SearchCenterPosition {get;init;}
public string SearchCenterName {get;init;}="";
public int MaxLegs { get; init; } = 4;
public bool Loop { get; init; } = true;
public bool CommunityGoalSupplyOnly { get; init; }
public string[] CommunityGoalCommodityFilter {get;init;}=[];
public long? SourceMarketIdFilter{get;init;}
public long? DestinationMarketIdFilter{get;init;}
public string SourceSystemFilter{get;init;}="";
public string SourceStationFilter{get;init;}="";
public string DestinationSystemFilter{get;init;}="";
public string DestinationStationFilter{get;init;}="";
public RouteObjective Objective { get; init; } = RouteObjective.CreditsPerHour;
public RiskTolerance Risk { get; init; } = RiskTolerance.Balanced;
public LandingPad RequiredPad { get; init; } = LandingPad.Large;
public string ActualShipType {get;init;}="";
public LandingPad ShipRequiredPad {get;init;}=LandingPad.Unknown;
public LandingPad EffectiveRequiredPad=>Planning.ShipPadPolicy.EffectiveRequiredPad(RequiredPad,ShipRequiredPad,ActualShipType);
public StationSourceKinds AllowedSourceKinds {get;init;}=StationSourceKinds.AllKnown;
public bool IncludePlanetary { get; init; }
public bool IncludeCarriers { get; init; }
public bool AllowUnknownMetadata { get; init; }
public int MaxMarketAgeMinutes { get; init; } = 360;
public double MaxArrivalDistanceLs { get; init; } = 10000;
public double MinutesPerJump { get; init; } = 1;
public double DockingMinutes { get; init; } = 4;
public decimal CostPerJump { get; init; } = 100;
public decimal CostPerMinute { get; init; }
public decimal MinimumProfitPerTon { get; init; } = 100;
public int BeamWidth { get; init; } = 48;
public int CargoSearchNodes { get; init; } = 2500;
public DateTimeOffset AsOf { get; init; } = DateTimeOffset.UtcNow;
public void Validate()
{
if(SourceMarketIdFilter is <=0||DestinationMarketIdFilter is <=0)throw new ArgumentException("明确指定的站点身份须有效;不指定时请留空。");
if (Credits <= ReserveCredits || ReserveCredits < 0) throw new ArgumentException("可用现金必须高于保留资金。");
if (InvestmentCredits is <= 0) throw new ArgumentException("固定投入金额必须大于零;使用全部可用资金时应留空。");
if (CargoCapacity < 1) throw new ArgumentException("实际货舱容量须大于零。");
if (OccupiedCargo < 0 || OccupiedCargo >= CargoCapacity) throw new ArgumentException("现有货物必须少于货舱总容量;请先腾出可用货舱。");
if (!double.IsFinite(LadenJumpRangeLy) || LadenJumpRangeLy is <= 0 or > 500) throw new ArgumentException("请填写有效的满载跳跃距离。");
if (!double.IsFinite(EstimatedJumpRangeFactor) || EstimatedJumpRangeFactor is <= 0 or > 1) throw new ArgumentException("估算满载跳距的保守系数须大于零且不超过 1。");
if (CurrentPosition is { IsValid: false }) throw new ArgumentException("当前位置坐标无效,无法估算前往起始站点的时间。");
if(SearchCenterPosition is{IsValid:false})throw new ArgumentException("搜索中心坐标无效。");
if (!double.IsFinite(MaxLegDistanceLy) || MaxLegDistanceLy <= 0 || !double.IsFinite(SearchRadiusLy) || SearchRadiusLy <= 0) throw new ArgumentException("搜索半径与航段距离必须大于零。");
if (MaxLegs is < 1 or > 8 || Loop && MaxLegs < 2) throw new ArgumentException("航段数量须在 1–8 之间,环线至少需要 2 段。");
if (MaxMarketAgeMinutes is < 1 or > 10080) throw new ArgumentException("行情时效须在 1 分钟至 7 天内。");
if (CostPerJump < 0 || CostPerMinute < 0 || MinimumProfitPerTon < 0 || !double.IsFinite(MinutesPerJump) || MinutesPerJump <= 0 || !double.IsFinite(DockingMinutes) || DockingMinutes <= 0 || !double.IsFinite(MaxArrivalDistanceLs) || MaxArrivalDistanceLs <= 0) throw new ArgumentException("时间、费用与距离参数无效。");
if (BeamWidth is < 1 or > 128 || CargoSearchNodes is < 1 or > 20000) throw new ArgumentException("搜索预算超出范围。");
}
}