diff --git a/src/XIVLauncher.Common/Game/Headlines.cs b/src/XIVLauncher.Common/Game/Headlines.cs index 4d664547b..79a5ff795 100644 --- a/src/XIVLauncher.Common/Game/Headlines.cs +++ b/src/XIVLauncher.Common/Game/Headlines.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Text; using System.Threading.Tasks; @@ -18,9 +19,6 @@ public partial class Headlines [JsonProperty("pinned")] public News[] Pinned { get; set; } - - [JsonProperty("banner")] - public Banner[] Banner { get; set; } } public class Banner @@ -30,6 +28,18 @@ public class Banner [JsonProperty("link")] public Uri Link { get; set; } + + [JsonProperty("order_priority")] + public int? OrderPriority { get; set; } + + [JsonProperty("fix_order")] + public int? FixOrder { get; set; } + } + + public class BannerRoot + { + [JsonProperty("banner")] + public List Banner { get; set; } } public class News @@ -52,7 +62,7 @@ public class News public partial class Headlines { - public static async Task Get(Launcher game, ClientLanguage language, bool forceNa = false) + public static async Task GetNews(Launcher game, ClientLanguage language, bool forceNa = false) { var unixTimestamp = ApiHelpers.GetUnixMillis(); var langCode = language.GetLangCode(forceNa); @@ -62,6 +72,38 @@ public static async Task Get(Launcher game, ClientLanguage language, return JsonConvert.DeserializeObject(json, Converter.SETTINGS); } + + public static async Task> GetBanners(Launcher game, ClientLanguage language, bool forceNa = false) + { + var unixTimestamp = ApiHelpers.GetUnixMillis(); + var langCode = language.GetLangCode(forceNa); + var url = $"https://frontier.ffxiv.com/v2/topics/{langCode}/banner.json?lang={langCode}&media=pcapp&_={unixTimestamp}"; + + var json = Encoding.UTF8.GetString(await game.DownloadAsLauncher(url, language, "application/json, text/plain, */*").ConfigureAwait(false)); + + return JsonConvert.DeserializeObject(json, Converter.SETTINGS).Banner; + } + + public static async Task> GetMessage(Launcher game, ClientLanguage language, bool forceNa = false) + { + var unixTimestamp = ApiHelpers.GetUnixMillis(); + var langCode = language.GetLangCode(forceNa); + var url = $"https://frontier.ffxiv.com/v2/notice/{langCode}/message.json?_={unixTimestamp}"; + + var json = Encoding.UTF8.GetString(await game.DownloadAsLauncher(url, language, "application/json, text/plain, */*").ConfigureAwait(false)); + + return JsonConvert.DeserializeObject(json, Converter.SETTINGS).Banner; + } + + public static async Task> GetWorlds(Launcher game, ClientLanguage language) + { + var unixTimestamp = ApiHelpers.GetUnixMillis(); + var url = $"https://frontier.ffxiv.com/v2/world/status.json?_={unixTimestamp}"; + + var json = Encoding.UTF8.GetString(await game.DownloadAsLauncher(url, language, "application/json, text/plain, */*").ConfigureAwait(false)); + + return JsonConvert.DeserializeObject(json, Converter.SETTINGS).Banner; + } } internal static class Converter diff --git a/src/XIVLauncher.Common/Game/Launcher.cs b/src/XIVLauncher.Common/Game/Launcher.cs index f9b28d13a..70c5eee90 100644 --- a/src/XIVLauncher.Common/Game/Launcher.cs +++ b/src/XIVLauncher.Common/Game/Launcher.cs @@ -1,5 +1,3 @@ - - #nullable enable using System; @@ -38,15 +36,16 @@ public class Launcher private readonly HttpClient client; private readonly string frontierUrlTemplate; - private const string FALLBACK_FRONTIER_URL_TEMPLATE = "https://launcher.finalfantasyxiv.com/v620/index.html?rc_lang={0}&time={1}"; - - public Launcher(ISteam? steam, IUniqueIdCache uniqueIdCache, ISettings settings, string? frontierUrl = null) + public Launcher(ISteam? steam, IUniqueIdCache uniqueIdCache, ISettings settings, string frontierUrl) { this.steam = steam; this.uniqueIdCache = uniqueIdCache; this.settings = settings; - this.frontierUrlTemplate = - string.IsNullOrWhiteSpace(frontierUrl) ? FALLBACK_FRONTIER_URL_TEMPLATE : frontierUrl; + + if (this.frontierUrlTemplate == null) + throw new Exception("Frontier URL template is null, this is now required"); + + this.frontierUrlTemplate = frontierUrl; ServicePointManager.Expect100Continue = false; @@ -71,7 +70,7 @@ public Launcher(ISteam? steam, IUniqueIdCache uniqueIdCache, ISettings settings, this.client = new HttpClient(handler); } - public Launcher(byte[] steamTicket, IUniqueIdCache uniqueIdCache, ISettings settings, string? frontierUrl = null) + public Launcher(byte[] steamTicket, IUniqueIdCache uniqueIdCache, ISettings settings, string frontierUrl) : this(steam: null, uniqueIdCache, settings, frontierUrl) { this.steamTicket = steamTicket; @@ -625,7 +624,7 @@ await DownloadAsLauncher( } } - public async Task GetLoginStatus() + public async Task GetLoginStatus() { try { @@ -633,11 +632,11 @@ public async Task GetLoginStatus() await DownloadAsLauncher( $"https://frontier.ffxiv.com/worldStatus/login_status.json?_={ApiHelpers.GetUnixMillis()}", ClientLanguage.English).ConfigureAwait(true)); - return Convert.ToBoolean(int.Parse(reply[10].ToString())); + return JsonConvert.DeserializeObject(reply); } catch (Exception exc) { - throw new Exception("Could not get gate status", exc); + throw new Exception("Could not get login status", exc); } } @@ -675,6 +674,7 @@ public async Task DownloadAsLauncher(string url, ClientLanguage language request.Headers.AddWithoutValidation("Origin", "https://launcher.finalfantasyxiv.com"); request.Headers.AddWithoutValidation("Referer", GenerateFrontierReferer(language)); + request.Headers.AddWithoutValidation("Connection", "Keep-Alive"); var resp = await this.client.SendAsync(request); return await resp.Content.ReadAsByteArrayAsync(); diff --git a/src/XIVLauncher/Windows/MainWindow.xaml b/src/XIVLauncher/Windows/MainWindow.xaml index c5f11a337..1d0e42893 100644 --- a/src/XIVLauncher/Windows/MainWindow.xaml +++ b/src/XIVLauncher/Windows/MainWindow.xaml @@ -174,7 +174,7 @@ + Width="Auto" Foreground="{Binding WorldStatusIconColor}"/> diff --git a/src/XIVLauncher/Windows/MainWindow.xaml.cs b/src/XIVLauncher/Windows/MainWindow.xaml.cs index 1c5c04d10..89420dafe 100644 --- a/src/XIVLauncher/Windows/MainWindow.xaml.cs +++ b/src/XIVLauncher/Windows/MainWindow.xaml.cs @@ -34,6 +34,7 @@ public partial class MainWindow : Window { private Timer _bannerChangeTimer; private Headlines _headlines; + private IReadOnlyList _banners; private BitmapImage[] _bannerBitmaps; private int _currentBannerIndex; private bool _everShown = false; @@ -121,14 +122,20 @@ private async Task SetupHeadlines() { _bannerChangeTimer?.Stop(); - _headlines = await Headlines.Get(_launcher, App.Settings.Language.GetValueOrDefault(ClientLanguage.English), App.Settings.ForceNorthAmerica.GetValueOrDefault(false)).ConfigureAwait(false); + await Headlines.GetWorlds(_launcher, App.Settings.Language.GetValueOrDefault(ClientLanguage.English)); + _banners = await Headlines.GetBanners(_launcher, App.Settings.Language.GetValueOrDefault(ClientLanguage.English), App.Settings.ForceNorthAmerica.GetValueOrDefault(false)) + .ConfigureAwait(false); + await Headlines.GetMessage(_launcher, App.Settings.Language.GetValueOrDefault(ClientLanguage.English), App.Settings.ForceNorthAmerica.GetValueOrDefault(false)) + .ConfigureAwait(false); + _headlines = await Headlines.GetNews(_launcher, App.Settings.Language.GetValueOrDefault(ClientLanguage.English), App.Settings.ForceNorthAmerica.GetValueOrDefault(false)) + .ConfigureAwait(false); - _bannerBitmaps = new BitmapImage[_headlines.Banner.Length]; + _bannerBitmaps = new BitmapImage[_banners.Count]; _bannerDotList = new(); - for (var i = 0; i < _headlines.Banner.Length; i++) + for (var i = 0; i < _banners.Count; i++) { - var imageBytes = await _launcher.DownloadAsLauncher(_headlines.Banner[i].LsbBanner.ToString(), App.Settings.Language.GetValueOrDefault(ClientLanguage.English)); + var imageBytes = await _launcher.DownloadAsLauncher(_banners[i].LsbBanner.ToString(), App.Settings.Language.GetValueOrDefault(ClientLanguage.English)); using var stream = new MemoryStream(imageBytes); @@ -157,7 +164,7 @@ private async Task SetupHeadlines() { _bannerDotList.ToList().ForEach(x => x.Active = false); - if (_currentBannerIndex + 1 > _headlines.Banner.Length - 1) + if (_currentBannerIndex + 1 > _banners.Count - 1) _currentBannerIndex = 0; else _currentBannerIndex++; @@ -176,8 +183,9 @@ private async Task SetupHeadlines() Dispatcher.BeginInvoke(new Action(() => { NewsListView.ItemsSource = _headlines.News; })); } - catch (Exception) + catch (Exception ex) { + Log.Error(ex, "Could not get news"); Dispatcher.BeginInvoke(new Action(() => { NewsListView.ItemsSource = new List {new News {Title = Loc.Localize("NewsDlFailed", "Could not download news data."), Tag = "DlError"}}; @@ -272,23 +280,6 @@ public void Initialize() this.SetDefaults(); - var worldStatusBrushOk = WorldStatusPackIcon.Foreground; - // grey out world status icon while deferred check is running - WorldStatusPackIcon.Foreground = new SolidColorBrush(Color.FromRgb(38, 38, 38)); - - _launcher.GetGateStatus(App.Settings.Language.GetValueOrDefault(ClientLanguage.English)).ContinueWith((resultTask) => - { - try - { - var brushToSet = resultTask.Result.Status ? worldStatusBrushOk : null; - Dispatcher.InvokeAsync(() => WorldStatusPackIcon.Foreground = brushToSet ?? new SolidColorBrush(Color.FromRgb(242, 24, 24))); - } - catch - { - // ignored - } - }); - _accountManager = new AccountManager(App.Settings); var savedAccount = _accountManager.CurrentAccount; @@ -359,7 +350,7 @@ private void BannerCard_MouseUp(object sender, MouseButtonEventArgs e) if (e.ChangedButton != MouseButton.Left) return; - if (_headlines != null) Process.Start(_headlines.Banner[_currentBannerIndex].Link.ToString()); + if (_headlines != null) Process.Start(_banners[_currentBannerIndex].Link.ToString()); } private void NewsListView_OnMouseUp(object sender, MouseButtonEventArgs e) diff --git a/src/XIVLauncher/Windows/ViewModel/MainWindowViewModel.cs b/src/XIVLauncher/Windows/ViewModel/MainWindowViewModel.cs index 523eef882..e347b7e18 100644 --- a/src/XIVLauncher/Windows/ViewModel/MainWindowViewModel.cs +++ b/src/XIVLauncher/Windows/ViewModel/MainWindowViewModel.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using System.Windows; using System.Windows.Input; +using System.Windows.Media; using System.Windows.Threading; using CheapLoc; using Serilog; @@ -36,6 +37,9 @@ internal class MainWindowViewModel : INotifyPropertyChanged { private readonly Window _window; + private readonly Task loginStatusTask; + private bool refetchLoginStatus = false; + public bool IsLoggingIn; public Launcher Launcher { get; private set; } @@ -71,9 +75,36 @@ public MainWindowViewModel(Window window) LoginNoThirdCommand = new SyncCommand(GetLoginFunc(AfterLoginAction.StartWithoutThird), () => !IsLoggingIn); LoginRepairCommand = new SyncCommand(GetLoginFunc(AfterLoginAction.Repair), () => !IsLoggingIn); + var frontierUrl = Updates.UpdateLease?.FrontierUrl; +#if DEBUG || RELEASENOUPDATE + // FALLBACK + frontierUrl ??= "https://launcher.finalfantasyxiv.com/v650/index.html?rc_lang={0}&time={1}"; +#endif + Launcher = App.GlobalSteamTicket == null - ? new(App.Steam, App.UniqueIdCache, CommonSettings.Instance, Updates.UpdateLease?.FrontierUrl) - : new(App.GlobalSteamTicket, App.UniqueIdCache, CommonSettings.Instance, Updates.UpdateLease?.FrontierUrl); + ? new(App.Steam, App.UniqueIdCache, CommonSettings.Instance, frontierUrl) + : new(App.GlobalSteamTicket, App.UniqueIdCache, CommonSettings.Instance, frontierUrl); + + // Tried and failed to get this from the theme + var worldStatusBrushOk = new SolidColorBrush(Color.FromRgb(0x21, 0x96, 0xf3)); + WorldStatusIconColor = worldStatusBrushOk; + + // Grey out world status icon while deferred check is running + WorldStatusIconColor = new SolidColorBrush(Color.FromRgb(38, 38, 38)); + + this.loginStatusTask = Launcher.GetLoginStatus(); + this.loginStatusTask.ContinueWith((resultTask) => + { + try + { + var brushToSet = resultTask.Result.Status ? worldStatusBrushOk : null; + WorldStatusIconColor = brushToSet ?? new SolidColorBrush(Color.FromRgb(242, 24, 24)); + } + catch + { + // ignored + } + }); } private Action GetLoginFunc(AfterLoginAction action) @@ -338,7 +369,17 @@ private async Task CheckGateStatus() #if !DEBUG try { - loginStatus = await Launcher.GetLoginStatus().ConfigureAwait(false); + if (refetchLoginStatus) + { + var response = await Launcher.GetLoginStatus().ConfigureAwait(false); + loginStatus = response.Status; + } + else + { + var response = await this.loginStatusTask; + loginStatus = response.Status; + refetchLoginStatus = true; + } } catch (Exception ex) { @@ -1485,6 +1526,17 @@ public string LoadingDialogMessage } } + private SolidColorBrush _worldStatusIconColor; + public SolidColorBrush WorldStatusIconColor + { + get => _worldStatusIconColor; + set + { + _worldStatusIconColor = value; + OnPropertyChanged(nameof(WorldStatusIconColor)); + } + } + #endregion #region Localization