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;
}
/// <summary>One notification per completed run, after the actual arrival has been confirmed.</summary>
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);
}
}
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;
}
/// <summary>One notification per completed run, after the actual arrival has been confirmed.</summary>
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);
}
}