using System.Text.Json;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
namespace XFEEDToolBox.Core.Execution;
public sealed record KnownStationFine(long MarketId,string Authority,long Amount);
public sealed record StationFineSnapshot(string CommanderId,IReadOnlyDictionary<long,KnownStationFine> Restrictions,int JournalFilesRead)
{
public long FineAt(long market)=>Restrictions.TryGetValue(market,out var fine)?fine.Amount:0;
}
/// <summary>Waits for known journal-confirmed fines affecting this station, without submitting a trade.</summary>
public static class StationAccessWaiter
{
public static async Task<long> KnownFineAsync(string journals,string commander,long market,CancellationToken token)
=>(await ReadKnownFinesAsync(journals,commander,token)).FineAt(market);
/// <summary>One journal pass resolves all known markets for this FID; no per-candidate file scans or guessed access flags.</summary>
public static async Task<StationFineSnapshot> ReadKnownFinesAsync(string? journals,string commander,CancellationToken token)
{
token.ThrowIfCancellationRequested();
if(journals is null||!Directory.Exists(journals))return new(commander,new Dictionary<long,KnownStationFine>(),0);
var fines=new Dictionary<string,long>(StringComparer.Ordinal);var authorities=new Dictionary<long,string>();var filesRead=0;
foreach(var file in Directory.EnumerateFiles(journals,"Journal*.log").Order(StringComparer.Ordinal))
{
token.ThrowIfCancellationRequested();filesRead++;
await using var stream=new FileStream(file,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);using var reader=new StreamReader(stream);var fid="";
while(await reader.ReadLineAsync(token) is {} line)
{
if(!line.Contains("\"LoadGame\"",StringComparison.Ordinal)&&!line.Contains("\"Docked\"",StringComparison.Ordinal)&&!line.Contains("\"Location\"",StringComparison.Ordinal)
&&!line.Contains("\"CommitCrime\"",StringComparison.Ordinal)&&!line.Contains("\"PayFines\"",StringComparison.Ordinal))continue;
JsonDocument document;try{document=JsonDocument.Parse(line);}catch(JsonException){continue;}
using(document)
{
var row=document.RootElement;string Text(string name)=>row.TryGetProperty(name,out var v)&&v.ValueKind==JsonValueKind.String?v.GetString()!:"";
long Number(string name)=>row.TryGetProperty(name,out var v)&&v.ValueKind==JsonValueKind.Number&&v.TryGetInt64(out var n)?n:0;
var kind=Text("event");if(kind=="LoadGame")fid=Text("FID");if(fid!=commander)continue;
var market=Number("MarketID");
if(kind is "Location" or "Docked"&&market>0&&row.TryGetProperty("StationFaction",out var station)
&&station.ValueKind==JsonValueKind.Object&&station.TryGetProperty("Name",out var name)&&name.ValueKind==JsonValueKind.String)
authorities[market]=name.GetString()??"";
var authority=Text("Faction");
if(kind=="CommitCrime"&&Number("Fine")>0&&authority.Length>0)fines[authority]=checked(fines.GetValueOrDefault(authority)+Number("Fine"));
if(kind=="PayFines")
{
if(row.TryGetProperty("AllFines",out var all)&&all.ValueKind==JsonValueKind.True)fines.Clear();
else if(authority.Length>0)fines[authority]=Math.Max(0,fines.GetValueOrDefault(authority)-Number("Amount"));
}
}
}
}
return new(commander,authorities.Where(pair=>fines.GetValueOrDefault(pair.Value)>0)
.ToDictionary(pair=>pair.Key,pair=>new KnownStationFine(pair.Key,pair.Value,fines[pair.Value])),filesRead);
}
public static async Task WaitAsync(string? journals,Func<PlayerState> player,long market,Action<string>? progress,CancellationToken token)
{
if(journals is null)return;
var initial=player();var schedule=new StationAccessWaitSchedule();
void RequireCurrent()
{
token.ThrowIfCancellationRequested();var current=player();
if(!current.InGame||current.CommanderId!=initial.CommanderId||current.CurrentShipId!=initial.CurrentShipId||current.LoadGameAt!=initial.LoadGameAt)
throw new InvalidDataException("等待站点交易资格时飞船或会话发生变化,已停止交易。");
if(!current.Docked||current.MarketId!=market)throw new InvalidOperationException("已离开等待中的交易站点,未提交订单。");
if(NavigationSafety.UnhealthyHull(current))throw new FlightHealthPauseException();
if(NavigationSafety.HasExternalThreat(current))throw new FlightThreatPauseException();
}
while(true)
{
RequireCurrent();
var fine=await KnownFineAsync(journals,initial.CommanderId,market,token);RequireCurrent();if(fine<=0)return;
var wait=schedule.Observe(fine);
if(wait.Report)progress?.Invoke($"本站有未缴罚款 {fine:N0} Cr,请在行政联系人缴纳;程序保留原货物并等待,缴清后自动继续。");
RequireCurrent();
await Task.Delay(wait.Delay,token);
}
}
}
/// <summary>Unchanged local fines do not create a one-second log loop or repeated full journal scans.</summary>
public sealed class StationAccessWaitSchedule
{
private static readonly int[] Delays=[1,2,4,8,15,30];
private long? previous;private int index;
public (bool Report,TimeSpan Delay) Observe(long fine)
{
if(fine<=0)throw new ArgumentOutOfRangeException(nameof(fine));
var changed=previous!=fine;if(changed){previous=fine;index=0;}
var delay=TimeSpan.FromSeconds(Delays[index]);index=Math.Min(index+1,Delays.Length-1);
return(changed,delay);
}
}
using System.Text.Json;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
namespace XFEEDToolBox.Core.Execution;
public sealed record KnownStationFine(long MarketId,string Authority,long Amount);
public sealed record StationFineSnapshot(string CommanderId,IReadOnlyDictionary<long,KnownStationFine> Restrictions,int JournalFilesRead)
{
public long FineAt(long market)=>Restrictions.TryGetValue(market,out var fine)?fine.Amount:0;
}
/// <summary>Waits for known journal-confirmed fines affecting this station, without submitting a trade.</summary>
public static class StationAccessWaiter
{
public static async Task<long> KnownFineAsync(string journals,string commander,long market,CancellationToken token)
=>(await ReadKnownFinesAsync(journals,commander,token)).FineAt(market);
/// <summary>One journal pass resolves all known markets for this FID; no per-candidate file scans or guessed access flags.</summary>
public static async Task<StationFineSnapshot> ReadKnownFinesAsync(string? journals,string commander,CancellationToken token)
{
token.ThrowIfCancellationRequested();
if(journals is null||!Directory.Exists(journals))return new(commander,new Dictionary<long,KnownStationFine>(),0);
var fines=new Dictionary<string,long>(StringComparer.Ordinal);var authorities=new Dictionary<long,string>();var filesRead=0;
foreach(var file in Directory.EnumerateFiles(journals,"Journal*.log").Order(StringComparer.Ordinal))
{
token.ThrowIfCancellationRequested();filesRead++;
await using var stream=new FileStream(file,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);using var reader=new StreamReader(stream);var fid="";
while(await reader.ReadLineAsync(token) is {} line)
{
if(!line.Contains("\"LoadGame\"",StringComparison.Ordinal)&&!line.Contains("\"Docked\"",StringComparison.Ordinal)&&!line.Contains("\"Location\"",StringComparison.Ordinal)
&&!line.Contains("\"CommitCrime\"",StringComparison.Ordinal)&&!line.Contains("\"PayFines\"",StringComparison.Ordinal))continue;
JsonDocument document;try{document=JsonDocument.Parse(line);}catch(JsonException){continue;}
using(document)
{
var row=document.RootElement;string Text(string name)=>row.TryGetProperty(name,out var v)&&v.ValueKind==JsonValueKind.String?v.GetString()!:"";
long Number(string name)=>row.TryGetProperty(name,out var v)&&v.ValueKind==JsonValueKind.Number&&v.TryGetInt64(out var n)?n:0;
var kind=Text("event");if(kind=="LoadGame")fid=Text("FID");if(fid!=commander)continue;
var market=Number("MarketID");
if(kind is "Location" or "Docked"&&market>0&&row.TryGetProperty("StationFaction",out var station)
&&station.ValueKind==JsonValueKind.Object&&station.TryGetProperty("Name",out var name)&&name.ValueKind==JsonValueKind.String)
authorities[market]=name.GetString()??"";
var authority=Text("Faction");
if(kind=="CommitCrime"&&Number("Fine")>0&&authority.Length>0)fines[authority]=checked(fines.GetValueOrDefault(authority)+Number("Fine"));
if(kind=="PayFines")
{
if(row.TryGetProperty("AllFines",out var all)&&all.ValueKind==JsonValueKind.True)fines.Clear();
else if(authority.Length>0)fines[authority]=Math.Max(0,fines.GetValueOrDefault(authority)-Number("Amount"));
}
}
}
}
return new(commander,authorities.Where(pair=>fines.GetValueOrDefault(pair.Value)>0)
.ToDictionary(pair=>pair.Key,pair=>new KnownStationFine(pair.Key,pair.Value,fines[pair.Value])),filesRead);
}
public static async Task WaitAsync(string? journals,Func<PlayerState> player,long market,Action<string>? progress,CancellationToken token)
{
if(journals is null)return;
var initial=player();var schedule=new StationAccessWaitSchedule();
void RequireCurrent()
{
token.ThrowIfCancellationRequested();var current=player();
if(!current.InGame||current.CommanderId!=initial.CommanderId||current.CurrentShipId!=initial.CurrentShipId||current.LoadGameAt!=initial.LoadGameAt)
throw new InvalidDataException("等待站点交易资格时飞船或会话发生变化,已停止交易。");
if(!current.Docked||current.MarketId!=market)throw new InvalidOperationException("已离开等待中的交易站点,未提交订单。");
if(NavigationSafety.UnhealthyHull(current))throw new FlightHealthPauseException();
if(NavigationSafety.HasExternalThreat(current))throw new FlightThreatPauseException();
}
while(true)
{
RequireCurrent();
var fine=await KnownFineAsync(journals,initial.CommanderId,market,token);RequireCurrent();if(fine<=0)return;
var wait=schedule.Observe(fine);
if(wait.Report)progress?.Invoke($"本站有未缴罚款 {fine:N0} Cr,请在行政联系人缴纳;程序保留原货物并等待,缴清后自动继续。");
RequireCurrent();
await Task.Delay(wait.Delay,token);
}
}
}
/// <summary>Unchanged local fines do not create a one-second log loop or repeated full journal scans.</summary>
public sealed class StationAccessWaitSchedule
{
private static readonly int[] Delays=[1,2,4,8,15,30];
private long? previous;private int index;
public (bool Report,TimeSpan Delay) Observe(long fine)
{
if(fine<=0)throw new ArgumentOutOfRangeException(nameof(fine));
var changed=previous!=fine;if(changed){previous=fine;index=0;}
var delay=TimeSpan.FromSeconds(Delays[index]);index=Math.Min(index+1,Delays.Length-1);
return(changed,delay);
}
}