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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
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("搜索预算超出范围。");
    }
}