using System.Xml.Linq; using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Navigation; using XFEEDToolBox.Core.Localization; namespace XFEEDToolBox.Core.Notifications; public sealed record NavigationArrival(string Id,long SystemAddress,string SystemName,string Destination,bool Docked, bool LocalNavigation,string CommanderId,DateTimeOffset? SessionAt,DateTimeOffset ArrivedAt) { public string Title=>$"已抵达 {Destination}"; public string Detail=>Docked?$"{SystemName} · 已完成停靠":LocalNavigation?$"{SystemName} · 已到达指定位置":"自动导航已完成"; public string Page=>LocalNavigation?"current-system":"navigation"; public bool SameScene(PlayerState player)=>player.InGame&&player.CommanderId==CommanderId&&player.LoadGameAt==SessionAt &&player.Navigation.CurrentSystemAddress==SystemAddress&&!player.Navigation.JumpInProgress; } /// One notification per completed run, after the actual arrival has been confirmed. public sealed class NavigationArrivalObserver { private readonly object gate=new(); private (string Commander,DateTimeOffset? Session,string Destination)? interstellarRun; private (string Commander,DateTimeOffset? Session,SystemNavigationTarget Target)? localRun; public NavigationArrival? Observe(NavigationProgress progress,PlayerState player,DateTimeOffset now) { lock(gate) { if(progress.State==NavigationRunState.Running){interstellarRun??=(player.CommanderId,player.LoadGameAt,progress.Destination.Trim());return null;} var run=interstellarRun;interstellarRun=null; if(run is not {} started||progress.State!=NavigationRunState.Completed||!Valid(player) ||started.Commander!=player.CommanderId||started.Session!=player.LoadGameAt ||!started.Destination.Equals(progress.Destination.Trim(),StringComparison.OrdinalIgnoreCase) ||!player.SystemName.Equals(progress.Destination.Trim(),StringComparison.OrdinalIgnoreCase))return null; return Create(player,player.SystemName,false,now); } } public NavigationArrival? Observe(SystemNavigationProgress progress,PlayerState player,DateTimeOffset now) { lock(gate) { if(progress.State==NavigationRunState.Running){localRun??=(player.CommanderId,player.LoadGameAt,progress.Target);return null;} var run=localRun;localRun=null; if(run is not {} started||progress.State!=NavigationRunState.Completed||!Valid(player) ||started.Commander!=player.CommanderId||started.Session!=player.LoadGameAt||!started.Target.SameIdentity(progress.Target) ||progress.Target.SystemAddress!=player.Navigation.CurrentSystemAddress ||progress.Target.NeedsDocking&&(!player.Docked||player.MarketId!=progress.Target.MarketId))return null; return Create(player,GameNavigationNames.Location(progress.Target.Name,"指定位置"),true,now); } } private static bool Valid(PlayerState player)=>player.InGame&&player.Navigation.CurrentSystemAddress is >0&&!player.Navigation.JumpInProgress; private static NavigationArrival Create(PlayerState player,string destination,bool local,DateTimeOffset now) =>new(Guid.NewGuid().ToString("N"),player.Navigation.CurrentSystemAddress!.Value,player.SystemName,destination, player.Docked,local,player.CommanderId,player.LoadGameAt,now); } public static class ArrivalToastContent { // Windows owns the outer toast. The hero is a full-width screenshot card, // with its own readable caption; it is never a separate simulated window. public static string Build(NavigationArrival arrival,string? heroPath=null) =>BuildCore(arrival.Title,arrival.Detail,$"action=arrival&page={arrival.Page}&system={arrival.SystemAddress}", $"{arrival.SystemName} 抵达时的游戏画面",heroPath); public static string Build(SessionReadyNotification ready,string? heroPath=null) =>BuildCore(ready.Title,ready.Detail,"action=session&page=home",$"{ready.Scene.SystemName} 当前游戏画面",heroPath); private static string BuildCore(string title,string detail,string launch,string imageDescription,string? heroPath) { var binding=new XElement("binding",new XAttribute("template","ToastGeneric"), new XElement("text",title),new XElement("text",detail)); if(heroPath is not null) { var file=new Uri(Path.GetFullPath(heroPath)); binding.AddFirst(new XElement("image",new XAttribute("placement","hero"),new XAttribute("src",file.AbsoluteUri), new XAttribute("alt",imageDescription))); } return new XElement("toast",new XAttribute("launch",launch), new XAttribute("duration","long"),new XElement("visual",binding), new XElement("audio",new XAttribute("src","ms-winsoundevent:Notification.Default"))).ToString(SaveOptions.DisableFormatting); } }