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 Quotes, DateTimeOffset CollectedAt, DataSource Source); public sealed record MarketBook(IReadOnlyList Stations, IReadOnlyList 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 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 Inventory { get; init; } = []; public IReadOnlyList Materials { get; init; } = []; public IReadOnlyList Engineers { get; init; } = []; public DateTimeOffset? EngineersAt { get; init; } public int VerifiedEngineeringRolls { get; init; } public int MismatchedEngineeringRolls { get; init; } public IReadOnlyList Locker { get; init; } = []; public IReadOnlyList Backpack { get; init; } = []; public long? CurrentShipId { get; init; } public IReadOnlyList Ships { get; init; } = []; public IReadOnlyList 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 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 Legs { get; init; } = []; public IReadOnlyList 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 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("搜索预算超出范围。"); } }