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.Models;
using XFEEDToolBox.Core.Navigation;

namespace XFEEDToolBox.Core.Execution;

public sealed class FlightSessionRestartRequiredException():InvalidOperationException("飞船保护已触发客户端重启,等待原模式和原订单恢复。");

/// <summary>Does not authorize inputs: the continuous native health guard is required separately.</summary>
public sealed class AutomaticTradeSafetyGate(PlayerState initial)
{
    private DateTimeOffset? safeSince;
    private long? safeMarket;
    public TimeSpan StableDuration{get;init;}=TimeSpan.FromSeconds(10);
    public bool RequireDocked{get;init;}=true;
    public void Reset(){safeSince=null;safeMarket=null;}
    public void RequireSameIdentity(PlayerState current)
    {
        if(current.CommanderId!=initial.CommanderId||current.CurrentShipId!=initial.CurrentShipId||current.CargoCapacity!=initial.CargoCapacity
            ||current.LoadGameAt!=initial.LoadGameAt)
            throw new InvalidOperationException("安全等待期间身份、飞船、货舱或会话已改变,不能恢复旧控制。");
    }
    public bool Observe(PlayerState current,DateTimeOffset now,bool continuousHealthReady)
    {
        RequireSameIdentity(current);
        if(!current.InGame||RequireDocked&&(!current.Docked||current.MarketId is not >0)||!current.CargoReliable||!current.CreditsReliable
            ||current.StatusReadAt is not{} read||now-read>TimeSpan.FromSeconds(2)||read>now.AddSeconds(1)
            ||NavigationSafety.UnhealthyHull(current)||NavigationSafety.HasExternalThreat(current)||!continuousHealthReady)
        {Reset();return false;}
        if(safeMarket!=current.MarketId){Reset();safeMarket=current.MarketId;}
        safeSince??=now;return now-safeSince>=StableDuration;
    }
}

public sealed record TradeSourceThreat(long MarketId,string StationName,DateTimeOffset AttackedAt,DateTimeOffset AvoidUntil)
{
    public int Incidents{get;init;}=1;
    public static TradeSourceThreat Merge(TradeSourceThreat? previous,TradeSourceThreat incoming)
    {
        if(previous is not null&&previous.AttackedAt>=incoming.AttackedAt)return previous;
        var count=previous is null?1:incoming.AttackedAt-previous.AttackedAt<TimeSpan.FromMinutes(2)?previous.Incidents:Math.Min(32,previous.Incidents+1);
        var until=incoming.AttackedAt.AddHours(count==1?1:count==2?24:72);
        return incoming with{Incidents=count,AvoidUntil=previous is not null&&previous.AvoidUntil>until?previous.AvoidUntil:until};
    }
}

/// <summary>Bounded, read-only evidence of attacks during a granted station approach, scoped to this commander and ship.</summary>
public static class TradeSourceThreatJournal
{
    public static IReadOnlyList<TradeSourceThreat> Read(string directory,PlayerState player,DateTimeOffset now)
    {
        if(!Directory.Exists(directory))return [];
        var incidents=new List<TradeSourceThreat>();
        foreach(var path in Directory.EnumerateFiles(directory,"Journal*.log").OrderByDescending(File.GetLastWriteTimeUtc).Take(8))
        {
            try
            {
                using var file=new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);
                using var header=new StreamReader(file,leaveOpen:true);string? commander=null;long? ship=null;
                for(var lineIndex=0;lineIndex<20&&header.ReadLine() is{} line;lineIndex++)
                {
                    using var doc=JsonDocument.Parse(line);var row=doc.RootElement;
                    if(row.TryGetProperty("event",out var kind)&&kind.GetString()=="LoadGame")
                    {commander=row.GetProperty("FID").GetString();ship=row.GetProperty("ShipID").GetInt64();break;}
                }
                if(commander!=player.CommanderId||ship!=player.CurrentShipId)continue;
                file.Seek(Math.Max(0,file.Length-262144),SeekOrigin.Begin);
                using var tail=new StreamReader(file,leaveOpen:true);if(file.Position>0)tail.ReadLine();
                long market=0;string station="";DateTimeOffset? granted=null;
                while(tail.ReadLine() is{} line)
                {
                    try
                    {
                        using var doc=JsonDocument.Parse(line);var row=doc.RootElement;
                        var kind=row.GetProperty("event").GetString();var at=row.GetProperty("timestamp").GetDateTimeOffset();
                        if(kind is "FSDJump" or "StartJump"){market=0;granted=null;}
                        if(kind=="Loadout"&&row.TryGetProperty("ShipID",out var id))ship=id.GetInt64();
                        if(kind is "DockingGranted" or "Undocked")
                        {market=row.GetProperty("MarketID").GetInt64();station=row.GetProperty("StationName").GetString()??"";granted=at;}
                        if(kind=="UnderAttack"&&ship==player.CurrentShipId&&market>0&&row.TryGetProperty("Target",out var target)&&target.GetString()=="You"
                            &&granted is{} permission&&at>=permission&&at-permission<TimeSpan.FromMinutes(10)&&at>now.AddHours(-1)&&at<=now.AddMinutes(1))
                            incidents.Add(new(market,station,at,at.AddHours(1)));
                    }
                    catch(Exception error)when(error is JsonException or KeyNotFoundException or FormatException or InvalidOperationException){/* Invalid appends are not combat witnesses. */}
                }
            }
            catch(Exception error)when(error is IOException or UnauthorizedAccessException or JsonException or KeyNotFoundException or FormatException or InvalidOperationException){/* Missing evidence cannot authorize recovery. */}
        }
        return incidents.GroupBy(i=>i.MarketId).Select(g=>g.MaxBy(i=>i.AttackedAt)!).ToArray();
    }
}