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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Navigation;

public static class ScoopRules
{
    public static bool CanRefuel(string starClass)
    {
        var value=starClass.Trim().ToUpperInvariant();
        // Public catalogues include descriptions such as "K (Yellow-Orange) Star".
        // Only an exact class or its explicit parenthesized description is accepted.
        return value.Length>0&&"OBAFGKM".Contains(value[0])&&(value.Length==1||value.StartsWith(value[0]+" (",StringComparison.Ordinal));
    }
}
public sealed record NavWaypoint(long SystemAddress,string Name,Coordinates Position,string StarClass)
{
    public bool Scoopable=>ScoopRules.CanRefuel(StarClass);
    public string RefuelText=>Scoopable?"可采集氢燃料":"不可采集氢燃料";
}
public sealed record NavigationJournalState
{
    public IReadOnlyList<NavWaypoint> Route { get; init; }=[];
    public DateTimeOffset? RouteAt { get; init; }
    public long? CurrentSystemAddress { get; init; }
    public DateTimeOffset? CurrentSystemAt { get; init; }
    public long? TargetSystemAddress { get; init; }
    public string TargetSystem { get; init; }="";
    public string TargetStarClass { get; init; }="";
    public int? RemainingJumps { get; init; }
    public DateTimeOffset? TargetAt { get; init; }
    public double? LastFuelUsed { get; init; }
    public double? MainFuel { get; init; }
    public DateTimeOffset? FuelAt { get; init; }
    public bool HeatAlert { get; init; }
    public DateTimeOffset? HeatAlertAt { get; init; }
    public bool ExternalThreat { get; init; }
    public DateTimeOffset? ExternalThreatAt { get; init; }
    public DateTimeOffset? InterdictedAt { get; init; }
    public bool JumpInProgress { get; init; }
    public long? JumpTargetSystemAddress { get; init; }
    public string JumpTargetSystem { get; init; }="";
    public DateTimeOffset? JumpStartedAt { get; init; }
    public DateTimeOffset? JumpStateAt { get; init; }
    public DateTimeOffset? LastJumpAt { get; init; }
    public DateTimeOffset? SupercruiseTransitionAt { get; init; }
    public DateTimeOffset? SupercruiseEnteredAt { get; init; }
    public DateTimeOffset? SupercruiseExitedAt { get; init; }
    public DateTimeOffset? UndockedAt { get; init; }
    public long? UndockedMarketId { get; init; }
    public string UndockedStationName { get; init; }="";
    public long? UndockedSystemAddress { get; init; }
    public bool DockingComputerActive { get; init; }
    public DateTimeOffset? DockingComputerAt { get; init; }
}

/// <summary>File/journal observations only; no inferred pose, heat percentage or native control.</summary>
public sealed class NavigationProjector
{
    private bool cleared;
    public NavigationJournalState State { get; private set; }=new();
    public void Reset(){State=new();cleared=false;}
    public void Apply(JsonElement e)
    {
        var at=e.Time()??DateTimeOffset.MinValue;var s=State;
        switch(e.Text("event"))
        {
            case "NavRoute" when e.Get("Route").ValueKind!=JsonValueKind.Array:
                if(!(s.RouteAt>at))cleared=false;break;
            case "NavRoute" when e.Get("Route").ValueKind==JsonValueKind.Array && !(s.RouteAt>at) && !(cleared&&s.RouteAt==at):
                var route=new List<NavWaypoint>();
                foreach(var item in e.Get("Route").Items())
                {
                    var coordinates=item.Position();var address=item.Long("SystemAddress");var name=item.Text("StarSystem");
                    if(address<=0||name.Length==0||coordinates is not {IsValid:true})throw new InvalidDataException("游戏航线包含无效星系标识或坐标,未替换原航线。");
                    route.Add(new(address,name,coordinates,item.Text("StarClass")));
                    if(route.Count>25000)throw new InvalidDataException("游戏航线超出读取上限。");
                }
                s=s with{Route=route,RouteAt=at};cleared=false;break;
            case "NavRouteClear" when !(s.RouteAt>at):s=s with{Route=[],RouteAt=at,TargetSystemAddress=null,TargetSystem="",RemainingJumps=null,TargetAt=at};cleared=true;break;
            case "FSDTarget" when !(s.TargetAt>at):
                s=s with{TargetSystemAddress=e.Get("SystemAddress").ValueKind==JsonValueKind.Number?e.Long("SystemAddress"):null,TargetSystem=e.Text("Name"),TargetStarClass=e.Text("StarClass"),RemainingJumps=e.Number("RemainingJumpsInRoute") is {} left?(int)left:null,TargetAt=at};break;
            case "Location" or "FSDJump" or "CarrierJump" when !(s.CurrentSystemAt>at) && !(s.JumpStateAt>at):
                s=s with{CurrentSystemAddress=e.Get("SystemAddress").ValueKind==JsonValueKind.Number?e.Long("SystemAddress"):s.CurrentSystemAddress,CurrentSystemAt=at,JumpInProgress=false,JumpStateAt=at};
                if(e.Text("event")=="FSDJump")
                {
                    s=s with{LastFuelUsed=e.Number("FuelUsed"),LastJumpAt=at};
                    if(!(s.FuelAt>at))s=s with{MainFuel=e.Number("FuelLevel"),FuelAt=at};
                }
                break;
            case "StartJump" when !(s.JumpStateAt>at) && !(s.CurrentSystemAt>at) && !(s.LastJumpAt>=at):
                var hyperspace=e.Text("JumpType")=="Hyperspace";
                s=s with{JumpInProgress=hyperspace,JumpTargetSystemAddress=hyperspace&&e.Long("SystemAddress")>0?e.Long("SystemAddress"):null,JumpTargetSystem=hyperspace?e.Text("StarSystem"):"",JumpStartedAt=hyperspace?at:null,JumpStateAt=at,SupercruiseTransitionAt=hyperspace?s.SupercruiseTransitionAt:at};break;
            case "SupercruiseEntry" when !(s.SupercruiseEnteredAt>at):s=s with{SupercruiseEnteredAt=at};break;
            case "SupercruiseExit" when !(s.SupercruiseExitedAt>at):s=s with{SupercruiseExitedAt=at};break;
            case "Undocked" when !(s.UndockedAt>at):s=s with{UndockedAt=at,UndockedMarketId=e.Long("MarketID") is >0 and var undockedMarket?undockedMarket:null,
                UndockedStationName=e.Text("StationName"),UndockedSystemAddress=s.CurrentSystemAddress};break;
            case "Music" when !(s.DockingComputerAt>at):s=s with{DockingComputerActive=e.Text("MusicTrack")=="DockingComputer",DockingComputerAt=at};break;
            case "FuelScoop" when !(s.FuelAt>at):s=s with{MainFuel=e.Number("Total")??s.MainFuel,FuelAt=at};break;
            case "Status":
                if(!(s.FuelAt>at)&&e.Get("Fuel").Number("FuelMain") is {} fuel)s=s with{MainFuel=fuel,FuelAt=at};
                var dangerFlags=e.Long("Flags");
                if(!(s.ExternalThreatAt>at)&&(dangerFlags&(1L<<23))!=0)s=s with{ExternalThreat=true,ExternalThreatAt=at};
                else if((s.ExternalThreatAt is null||at>s.ExternalThreatAt)&&(dangerFlags&((1L<<22)|(1L<<23)))==0)
                    s=s with{ExternalThreat=false,ExternalThreatAt=at};
                if(!(s.HeatAlertAt>at))s=s with{HeatAlert=(e.Long("Flags")&(1L<<20))!=0,HeatAlertAt=at};break;
            case "Interdicted" when !(s.ExternalThreatAt>at):s=s with{ExternalThreat=true,ExternalThreatAt=at,InterdictedAt=at};break;
            case "UnderAttack" when !(s.ExternalThreatAt>at):s=s with{ExternalThreat=true,ExternalThreatAt=at};break;
            case "LoadGame" when !(s.ExternalThreatAt>at):s=s with{ExternalThreat=false,ExternalThreatAt=at,InterdictedAt=null,DockingComputerActive=false,DockingComputerAt=at};break;
            case "HeatWarning" or "HeatDamage" when !(s.HeatAlertAt>at):s=s with{HeatAlert=true,HeatAlertAt=at};break;
            case "Shutdown" when !(s.JumpStateAt>at):s=s with{JumpInProgress=false,JumpStateAt=at};break;
        }
        State=s;
    }
    public static IReadOnlyList<NavWaypoint> RemainingRoute(NavigationJournalState state,string destination)
    {
        if(state.Route.Count<1)throw new InvalidOperationException("游戏尚未生成航路,请先规划航线。");
        if(!state.Route[^1].Name.Equals(destination.Trim(),StringComparison.OrdinalIgnoreCase))throw new InvalidOperationException("游戏航线终点与软件中设定的目标星系不一致。");
        var index=state.Route.ToList().FindIndex(w=>w.SystemAddress==state.CurrentSystemAddress);
        if(index<0)throw new InvalidOperationException("当前星系不在已读取的游戏航线中,请重新规划。");
        var remaining=state.Route.Skip(index+1).ToArray();
        if(remaining.Length>0&&state.TargetSystemAddress is {} target&&target!=remaining[0].SystemAddress)throw new InvalidOperationException("游戏当前锁定的下一跳与航线不一致。");
        return remaining;
    }
}