using System.Globalization;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
namespace XFEEDToolBox.Core.Presentation;
public sealed record ShipStatusValue(string Key,string Name,string Original,string Value,string Source);
public sealed class ShipStatusLiveRow(ShipStatusValue value):LiveRow<ShipStatusValue>(value)
{
public string Name=>Record.Name;
public string Original=>Record.Original;
public string Value=>Record.Value;
public string Source=>Record.Source;
}
public sealed record ShipAmmunition(string Slot,string Name,string Original,string Clip,string Reserve,string State,string Source);
public sealed class ShipAmmunitionLiveRow(ShipAmmunition value):LiveRow<ShipAmmunition>(value)
{
public string Slot=>Record.Slot;
public string Name=>Record.Name;
public string Original=>Record.Original;
public string Clip=>Record.Clip;
public string Reserve=>Record.Reserve;
public string State=>Record.State;
public string Source=>Record.Source;
}
/// <summary>Formats observed values and their actual sources; an absent measurement stays unknown.</summary>
public static class ShipStatusRows
{
public static string At(DateTimeOffset? at)=>at?.ToLocalTime().ToString("HH:mm:ss",CultureInfo.InvariantCulture)??"无记录";
private static string F(double? value,string format="F1",string suffix="")=>value is {} n&&double.IsFinite(n)?n.ToString(format,CultureInfo.InvariantCulture)+suffix:"—";
private static double? N(JsonElement value,string key)=>value.ValueKind==JsonValueKind.Object&&value.TryGetProperty(key,out var property)&&property.ValueKind==JsonValueKind.Number&&property.TryGetDouble(out var n)&&double.IsFinite(n)?n:null;
private static JsonElement Object(JsonElement value,string key)=>value.ValueKind==JsonValueKind.Object&&value.TryGetProperty(key,out var property)&&property.ValueKind==JsonValueKind.Object?property:default;
private static double? Radius(JsonElement geometry)
{
if(geometry.ValueKind!=JsonValueKind.Object||!geometry.TryGetProperty("position",out var p)||p.ValueKind!=JsonValueKind.Array||p.GetArrayLength()!=3)return null;
var a=p.EnumerateArray().Select(v=>v.GetDouble()).ToArray();return Math.Sqrt(a.Sum(v=>v*v));
}
public static IReadOnlyList<ShipStatusValue> Flight(JsonElement? native)
{
var raw=native??default;var fsd=Object(raw,"fsd");var thermal=Object(raw,"thermal");var geometry=Object(raw,"geometry");var scoop=Object(geometry,"scoop");
var time=N(raw,"observedAtUnixMilliseconds");var source="原生 · "+At(time is {} t?DateTimeOffset.FromUnixTimeMilliseconds((long)t):null);
var radius=Radius(geometry);var exclusion=N(geometry,"exclusionRadius");var max=N(scoop,"maximumRate");var efficiency=N(scoop,"efficiency");
var active=scoop.ValueKind==JsonValueKind.Object&&scoop.TryGetProperty("active",out var a)&&a.ValueKind==JsonValueKind.True;
var scoopKnown=scoop.ValueKind==JsonValueKind.Object&&scoop.TryGetProperty("verified",out var sk)&&sk.ValueKind==JsonValueKind.True;
var hud=N(raw,"heatHudPercent");var verified=raw.ValueKind==JsonValueKind.Object&&raw.TryGetProperty("heatNormalizationVerified",out var v)&&v.ValueKind==JsonValueKind.True;
var state=N(fsd,"state");var clock=N(fsd,"clock");var duration=N(fsd,"duration");
var rows=new List<ShipStatusValue>();
void Add(string key,string name,string original,string value)=>rows.Add(new(key,name,original,value,source));
Add("heat","舰体热量","Heat",verified?F(hud,"F1","%" ):"待核对");
Add("heatRaw","内部热比例","Internal heat ratio",F(N(raw,"internalHeatPercent")??N(raw,"heatPercent"),"F2","%"));
Add("forecast","充能预测上界","Charge forecast",F(N(thermal,"fullChargeAndHoldPeak"),"F1",verified?"%":"% · 内部比例"));
var speed=N(raw,"speedMetresPerSecond");
Add("speed","实际速度","Speed",speed>=1e6?F(speed/1e6,"F2"," Mm/s"):F(speed,"F0"," m/s"));
Add("throttle","节流阀","Throttle",F(N(fsd,"throttle")*100,"F0","%"));
Add("angular","转向角速度","Angular speed",F(N(raw,"angularSpeedDegreesPerSecond"),"F2"," °/s"));
Add("fsd","跃迁引擎","Frame shift drive",state switch{0=>"未就绪",1=>"冷却中",2=>"就绪",3=>clock>=duration?"充能完成":"充能中",4=>"跃迁中",_=>"—"});
Add("charge","剩余充能","Charge remaining",state==3?F(Math.Max(0,(duration??0)-(clock??0)),"F1"," s"):"—");
Add("scoop","当前吸油速率","Fuel scoop rate",F(!scoopKnown?null:active?max*efficiency*1000:0,"F1"," kg/s"));
Add("scoopMax","额定吸油速率","Maximum scoop rate",F(max*1000,"F0"," kg/s"));
Add("efficiency","吸油效率","Scoop efficiency",F(!scoopKnown?null:active?efficiency*100:0,"F1","%"));
Add("radius","距恒星中心","Distance to star centre",F(radius/1e6,"F2"," Mm"));
Add("exclusion","超巡碰撞半径","Cruise exclusion radius",F(exclusion/1e6,"F2"," Mm"));
Add("ratio","当前 / 碰撞半径","Radius ratio",exclusion>0?F(radius/exclusion,"F4"," ×"):"—");
Add("fullRadius","满功率外沿半径","Full-rate outer radius",F(N(scoop,"fullRateRadius")/1e6,"F2"," Mm"));
return rows;
}
public static IReadOnlyList<ShipStatusValue> Cockpit(PlayerState p)
{
var ship=p.Ships.FirstOrDefault(s=>s.ShipId==p.CurrentShipId);var status=p.Status;var flags=status?.Flags;var source="Status · "+At(p.StatusAt);
string Flag(int bit,string yes,string no)=>flags is {} f?(f&(1L<<bit))!=0?yes:no:"—";
var rows=new List<ShipStatusValue>();
void Add(string key,string name,string original,string value,string? from=null)=>rows.Add(new(key,name,original,value,from??source));
var fuel=p.Navigation.FuelAt>p.StatusAt?p.Navigation.MainFuel:status?.FuelMain??p.Navigation.MainFuel;
Add("fuel","主油箱","Main fuel",F(fuel,"F2"," t")+" / "+F(ship?.FuelMain,"F2"," t"));
Add("reserve","备用油箱","Fuel reservoir",F(status?.FuelReserve,"F3"," t"));
Add("shield","护盾","Shields",Flag(3,"已升起","已失效"));
Add("hull","船体完整度","Hull integrity",F(ship?.HullHealth*100,"F1","%"),"Loadout 快照 · "+At(ship?.ConfigurationAt));
Add("pips","系统 / 引擎 / 武器配电","SYS / ENG / WEP",status?.Pips.Count==3?string.Join(" / ",status.Pips.Select(n=>(n/2d).ToString("g",CultureInfo.InvariantCulture))):"—");
Add("cargo","货舱","Cargo",$"{p.CargoCount} / {p.CargoCapacity} t");
Add("flight","飞行模式","Flight mode",Flag(4,"超巡","普通空间"));
Add("assist","飞行辅助","Flight assist",Flag(5,"关闭","开启"));
Add("gear","起落架","Landing gear",Flag(2,"展开","收起"));
Add("hardpoints","武器挂点","Hardpoints",Flag(6,"部署","收起"));
Add("scoopDoor","货舱舱门","Cargo scoop",Flag(9,"展开","收起"));
Add("silent","静默航行","Silent running",Flag(10,"开启","关闭"));
Add("masslock","质量锁定","Mass locked",Flag(16,"是","否"));
Add("danger","危险状态","In danger",Flag(22,"警报","正常"));
Add("heatWarning","过热警报","Overheating",Flag(20,"警报","正常"));
Add("lowFuel","低燃油警报","Low fuel",Flag(19,"警报","正常"));
Add("latitude","纬度","Latitude",F(status?.Latitude,"F5","°"));
Add("longitude","经度","Longitude",F(status?.Longitude,"F5","°"));
Add("altitude","海拔","Altitude",F(status?.Altitude,"F1"," m"));
Add("heading","航向","Heading",F(status?.Heading,"F1","°"));
Add("range","最大跳距","Maximum jump range",F(ship?.MaxJumpRange,"F2"," ly"),"Loadout 快照 · "+At(ship?.ConfigurationAt));
Add("rebuy","保险费用","Rebuy",ship?.Rebuy is {} rebuy?rebuy.ToString("N0",CultureInfo.InvariantCulture)+" Cr":"—","Loadout 快照 · "+At(ship?.ConfigurationAt));
return rows;
}
public static IReadOnlyList<ShipAmmunition> Ammunition(PlayerState p)
{
var ship=p.Ships.FirstOrDefault(s=>s.ShipId==p.CurrentShipId);
return ship?.Modules.Where(m=>m.AmmoInClip is not null||m.AmmoInHopper is not null).Select(m=>new ShipAmmunition(m.Slot,m.DisplayName,m.OriginalName,m.AmmoInClip?.ToString()??"—",m.AmmoInHopper?.ToString()??"—",$"{m.State} · 完整度 {m.HealthText}","Loadout 快照 · "+At(ship.ConfigurationAt))).ToArray()??[];
}
}
using System.Globalization;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
namespace XFEEDToolBox.Core.Presentation;
public sealed record ShipStatusValue(string Key,string Name,string Original,string Value,string Source);
public sealed class ShipStatusLiveRow(ShipStatusValue value):LiveRow<ShipStatusValue>(value)
{
public string Name=>Record.Name;
public string Original=>Record.Original;
public string Value=>Record.Value;
public string Source=>Record.Source;
}
public sealed record ShipAmmunition(string Slot,string Name,string Original,string Clip,string Reserve,string State,string Source);
public sealed class ShipAmmunitionLiveRow(ShipAmmunition value):LiveRow<ShipAmmunition>(value)
{
public string Slot=>Record.Slot;
public string Name=>Record.Name;
public string Original=>Record.Original;
public string Clip=>Record.Clip;
public string Reserve=>Record.Reserve;
public string State=>Record.State;
public string Source=>Record.Source;
}
/// <summary>Formats observed values and their actual sources; an absent measurement stays unknown.</summary>
public static class ShipStatusRows
{
public static string At(DateTimeOffset? at)=>at?.ToLocalTime().ToString("HH:mm:ss",CultureInfo.InvariantCulture)??"无记录";
private static string F(double? value,string format="F1",string suffix="")=>value is {} n&&double.IsFinite(n)?n.ToString(format,CultureInfo.InvariantCulture)+suffix:"—";
private static double? N(JsonElement value,string key)=>value.ValueKind==JsonValueKind.Object&&value.TryGetProperty(key,out var property)&&property.ValueKind==JsonValueKind.Number&&property.TryGetDouble(out var n)&&double.IsFinite(n)?n:null;
private static JsonElement Object(JsonElement value,string key)=>value.ValueKind==JsonValueKind.Object&&value.TryGetProperty(key,out var property)&&property.ValueKind==JsonValueKind.Object?property:default;
private static double? Radius(JsonElement geometry)
{
if(geometry.ValueKind!=JsonValueKind.Object||!geometry.TryGetProperty("position",out var p)||p.ValueKind!=JsonValueKind.Array||p.GetArrayLength()!=3)return null;
var a=p.EnumerateArray().Select(v=>v.GetDouble()).ToArray();return Math.Sqrt(a.Sum(v=>v*v));
}
public static IReadOnlyList<ShipStatusValue> Flight(JsonElement? native)
{
var raw=native??default;var fsd=Object(raw,"fsd");var thermal=Object(raw,"thermal");var geometry=Object(raw,"geometry");var scoop=Object(geometry,"scoop");
var time=N(raw,"observedAtUnixMilliseconds");var source="原生 · "+At(time is {} t?DateTimeOffset.FromUnixTimeMilliseconds((long)t):null);
var radius=Radius(geometry);var exclusion=N(geometry,"exclusionRadius");var max=N(scoop,"maximumRate");var efficiency=N(scoop,"efficiency");
var active=scoop.ValueKind==JsonValueKind.Object&&scoop.TryGetProperty("active",out var a)&&a.ValueKind==JsonValueKind.True;
var scoopKnown=scoop.ValueKind==JsonValueKind.Object&&scoop.TryGetProperty("verified",out var sk)&&sk.ValueKind==JsonValueKind.True;
var hud=N(raw,"heatHudPercent");var verified=raw.ValueKind==JsonValueKind.Object&&raw.TryGetProperty("heatNormalizationVerified",out var v)&&v.ValueKind==JsonValueKind.True;
var state=N(fsd,"state");var clock=N(fsd,"clock");var duration=N(fsd,"duration");
var rows=new List<ShipStatusValue>();
void Add(string key,string name,string original,string value)=>rows.Add(new(key,name,original,value,source));
Add("heat","舰体热量","Heat",verified?F(hud,"F1","%" ):"待核对");
Add("heatRaw","内部热比例","Internal heat ratio",F(N(raw,"internalHeatPercent")??N(raw,"heatPercent"),"F2","%"));
Add("forecast","充能预测上界","Charge forecast",F(N(thermal,"fullChargeAndHoldPeak"),"F1",verified?"%":"% · 内部比例"));
var speed=N(raw,"speedMetresPerSecond");
Add("speed","实际速度","Speed",speed>=1e6?F(speed/1e6,"F2"," Mm/s"):F(speed,"F0"," m/s"));
Add("throttle","节流阀","Throttle",F(N(fsd,"throttle")*100,"F0","%"));
Add("angular","转向角速度","Angular speed",F(N(raw,"angularSpeedDegreesPerSecond"),"F2"," °/s"));
Add("fsd","跃迁引擎","Frame shift drive",state switch{0=>"未就绪",1=>"冷却中",2=>"就绪",3=>clock>=duration?"充能完成":"充能中",4=>"跃迁中",_=>"—"});
Add("charge","剩余充能","Charge remaining",state==3?F(Math.Max(0,(duration??0)-(clock??0)),"F1"," s"):"—");
Add("scoop","当前吸油速率","Fuel scoop rate",F(!scoopKnown?null:active?max*efficiency*1000:0,"F1"," kg/s"));
Add("scoopMax","额定吸油速率","Maximum scoop rate",F(max*1000,"F0"," kg/s"));
Add("efficiency","吸油效率","Scoop efficiency",F(!scoopKnown?null:active?efficiency*100:0,"F1","%"));
Add("radius","距恒星中心","Distance to star centre",F(radius/1e6,"F2"," Mm"));
Add("exclusion","超巡碰撞半径","Cruise exclusion radius",F(exclusion/1e6,"F2"," Mm"));
Add("ratio","当前 / 碰撞半径","Radius ratio",exclusion>0?F(radius/exclusion,"F4"," ×"):"—");
Add("fullRadius","满功率外沿半径","Full-rate outer radius",F(N(scoop,"fullRateRadius")/1e6,"F2"," Mm"));
return rows;
}
public static IReadOnlyList<ShipStatusValue> Cockpit(PlayerState p)
{
var ship=p.Ships.FirstOrDefault(s=>s.ShipId==p.CurrentShipId);var status=p.Status;var flags=status?.Flags;var source="Status · "+At(p.StatusAt);
string Flag(int bit,string yes,string no)=>flags is {} f?(f&(1L<<bit))!=0?yes:no:"—";
var rows=new List<ShipStatusValue>();
void Add(string key,string name,string original,string value,string? from=null)=>rows.Add(new(key,name,original,value,from??source));
var fuel=p.Navigation.FuelAt>p.StatusAt?p.Navigation.MainFuel:status?.FuelMain??p.Navigation.MainFuel;
Add("fuel","主油箱","Main fuel",F(fuel,"F2"," t")+" / "+F(ship?.FuelMain,"F2"," t"));
Add("reserve","备用油箱","Fuel reservoir",F(status?.FuelReserve,"F3"," t"));
Add("shield","护盾","Shields",Flag(3,"已升起","已失效"));
Add("hull","船体完整度","Hull integrity",F(ship?.HullHealth*100,"F1","%"),"Loadout 快照 · "+At(ship?.ConfigurationAt));
Add("pips","系统 / 引擎 / 武器配电","SYS / ENG / WEP",status?.Pips.Count==3?string.Join(" / ",status.Pips.Select(n=>(n/2d).ToString("g",CultureInfo.InvariantCulture))):"—");
Add("cargo","货舱","Cargo",$"{p.CargoCount} / {p.CargoCapacity} t");
Add("flight","飞行模式","Flight mode",Flag(4,"超巡","普通空间"));
Add("assist","飞行辅助","Flight assist",Flag(5,"关闭","开启"));
Add("gear","起落架","Landing gear",Flag(2,"展开","收起"));
Add("hardpoints","武器挂点","Hardpoints",Flag(6,"部署","收起"));
Add("scoopDoor","货舱舱门","Cargo scoop",Flag(9,"展开","收起"));
Add("silent","静默航行","Silent running",Flag(10,"开启","关闭"));
Add("masslock","质量锁定","Mass locked",Flag(16,"是","否"));
Add("danger","危险状态","In danger",Flag(22,"警报","正常"));
Add("heatWarning","过热警报","Overheating",Flag(20,"警报","正常"));
Add("lowFuel","低燃油警报","Low fuel",Flag(19,"警报","正常"));
Add("latitude","纬度","Latitude",F(status?.Latitude,"F5","°"));
Add("longitude","经度","Longitude",F(status?.Longitude,"F5","°"));
Add("altitude","海拔","Altitude",F(status?.Altitude,"F1"," m"));
Add("heading","航向","Heading",F(status?.Heading,"F1","°"));
Add("range","最大跳距","Maximum jump range",F(ship?.MaxJumpRange,"F2"," ly"),"Loadout 快照 · "+At(ship?.ConfigurationAt));
Add("rebuy","保险费用","Rebuy",ship?.Rebuy is {} rebuy?rebuy.ToString("N0",CultureInfo.InvariantCulture)+" Cr":"—","Loadout 快照 · "+At(ship?.ConfigurationAt));
return rows;
}
public static IReadOnlyList<ShipAmmunition> Ammunition(PlayerState p)
{
var ship=p.Ships.FirstOrDefault(s=>s.ShipId==p.CurrentShipId);
return ship?.Modules.Where(m=>m.AmmoInClip is not null||m.AmmoInHopper is not null).Select(m=>new ShipAmmunition(m.Slot,m.DisplayName,m.OriginalName,m.AmmoInClip?.ToString()??"—",m.AmmoInHopper?.ToString()??"—",$"{m.State} · 完整度 {m.HealthText}","Loadout 快照 · "+At(ship.ConfigurationAt))).ToArray()??[];
}
}