diff --git a/EpicManifestParser b/EpicManifestParser index 97174265..21df8a55 160000 --- a/EpicManifestParser +++ b/EpicManifestParser @@ -1 +1 @@ -Subproject commit 97174265894eddcb0d94ef570d36ddc559a8573a +Subproject commit 21df8a55d474f14148a35bc943e06f3fdc20c997 diff --git a/FModel/MainWindow.xaml.cs b/FModel/MainWindow.xaml.cs index 5c6a5860..41850e26 100644 --- a/FModel/MainWindow.xaml.cs +++ b/FModel/MainWindow.xaml.cs @@ -69,9 +69,9 @@ public partial class MainWindow await _applicationView.CUE4Parse.InitInformation(); #endif await Task.WhenAll( - Task.Run(() => _applicationView.CUE4Parse.VerifyConsoleVariables()), - Task.Run(() => _applicationView.CUE4Parse.VerifyVirtualCache()), - Task.Run(() => _applicationView.CUE4Parse.VerifyContentBuildManifest()), + _applicationView.CUE4Parse.VerifyConsoleVariables(), + _applicationView.CUE4Parse.VerifyVirtualCache(), + _applicationView.CUE4Parse.VerifyContentBuildManifest(), _applicationView.CUE4Parse.InitMappings(), _applicationView.InitImGuiSettings(newOrUpdated), _applicationView.InitVgmStream(), @@ -84,9 +84,9 @@ public partial class MainWindow ).ConfigureAwait(false); #if DEBUG - await _threadWorkerView.Begin(cancellationToken => - _applicationView.CUE4Parse.Extract(cancellationToken, - "fortnitegame/Content/Characters/Player/Female/Medium/Bodies/F_MED_Ballerina/Meshes/F_MED_Ballerina.uasset")); + // await _threadWorkerView.Begin(cancellationToken => + // _applicationView.CUE4Parse.Extract(cancellationToken, + // "fortnitegame/Content/Characters/Player/Female/Medium/Bodies/F_MED_Ballerina/Meshes/F_MED_Ballerina.uasset")); #endif } diff --git a/FModel/ViewModels/ApiEndpoints/EpicApiEndpoint.cs b/FModel/ViewModels/ApiEndpoints/EpicApiEndpoint.cs index 02662519..e09390a2 100644 --- a/FModel/ViewModels/ApiEndpoints/EpicApiEndpoint.cs +++ b/FModel/ViewModels/ApiEndpoints/EpicApiEndpoint.cs @@ -13,7 +13,8 @@ public class EpicApiEndpoint : AbstractApiProvider { private const string _OAUTH_URL = "https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/token"; private const string _BASIC_TOKEN = "basic MzQ0NmNkNzI2OTRjNGE0NDg1ZDgxYjc3YWRiYjIxNDE6OTIwOWQ0YTVlMjVhNDU3ZmI5YjA3NDg5ZDMxM2I0MWE="; - private const string _LAUNCHER_ASSETS = "https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/v2/platform/Windows/namespace/fn/catalogItem/4fe75bbc5a674f4f9b356b5c90567da5/app/Fortnite/label/Live"; + private const string _APP_URL = "https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/v2/platform/Windows/namespace/fn/catalogItem/4fe75bbc5a674f4f9b356b5c90567da5/app/Fortnite/label/Live"; + private const string _CBM_URL = "https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/Windows/5cb97847cee34581afdbc445400e2f77/FortniteContentBuilds"; public EpicApiEndpoint(RestClient client) : base(client) { } @@ -28,11 +29,30 @@ public class EpicApiEndpoint : AbstractApiProvider } } - var request = new FRestRequest(_LAUNCHER_ASSETS); + var request = new FRestRequest(_APP_URL); request.AddHeader("Authorization", $"bearer {UserSettings.Default.LastAuthResponse.AccessToken}"); var response = await _client.ExecuteAsync(request, token).ConfigureAwait(false); Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString); - return new ManifestInfo(response.Content); + return response.IsSuccessful ? new ManifestInfo(response.Content) : null; + } + + public async Task GetContentBuildManifestAsync(CancellationToken token, string label) + { + if (await IsExpired().ConfigureAwait(false)) + { + var auth = await GetAuthAsync(token).ConfigureAwait(false); + if (auth != null) + { + UserSettings.Default.LastAuthResponse = auth; + } + } + + var request = new FRestRequest(_CBM_URL); + request.AddHeader("Authorization", $"bearer {UserSettings.Default.LastAuthResponse.AccessToken}"); + request.AddQueryParameter("label", label); + var response = await _client.ExecuteAsync(request, token).ConfigureAwait(false); + Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString); + return response.IsSuccessful ? new ContentBuildManifestInfo(response.Content) : null; } public ManifestInfo GetManifest(CancellationToken token) @@ -40,6 +60,11 @@ public class EpicApiEndpoint : AbstractApiProvider return GetManifestAsync(token).GetAwaiter().GetResult(); } + public ContentBuildManifestInfo GetContentBuildManifest(CancellationToken token, string label) + { + return GetContentBuildManifestAsync(token, label).GetAwaiter().GetResult(); + } + private async Task GetAuthAsync(CancellationToken token) { var request = new FRestRequest(_OAUTH_URL, Method.Post); diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index db757372..9b7199df 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -120,6 +120,7 @@ public class CUE4ParseViewModel : ViewModel public AssetsFolderViewModel AssetsFolder { get; } public SearchViewModel SearchVm { get; } public TabControlViewModel TabControl { get; } + public ConfigIni BuildInfo { get; } public CUE4ParseViewModel(string gameDirectory) { @@ -204,6 +205,7 @@ public class CUE4ParseViewModel : ViewModel AssetsFolder = new AssetsFolderViewModel(); SearchVm = new SearchViewModel(); TabControl = new TabControlViewModel(); + BuildInfo = new ConfigIni(nameof(BuildInfo)); } public async Task Initialize() @@ -244,18 +246,19 @@ public class CUE4ParseViewModel : ViewModel foreach (var fileManifest in manifest.FileManifests) { + if (fileManifest.Name.Equals("Cloud/BuildInfo.ini", StringComparison.OrdinalIgnoreCase)) + { + BuildInfo.Read(new StreamReader(fileManifest.GetStream())); + continue; + } if (!_fnLive.IsMatch(fileManifest.Name)) continue; - //var casStream = manifest.FileManifests.FirstOrDefault(x => x.Name.Equals(fileManifest.Name.Replace(".utoc", ".ucas"))); - //p.Initialize(fileManifest.Name, new[] {fileManifest.GetStream(), casStream.GetStream()}); p.Initialize(fileManifest.Name, new Stream[] { fileManifest.GetStream() } , it => new FStreamArchive(it, manifest.FileManifests.First(x => x.Name.Equals(it)).GetStream(), p.Versions)); } FLogger.Append(ELog.Information, () => FLogger.Text($"Fortnite has been loaded successfully in {manifest.ParseTime.TotalMilliseconds}ms", Constants.WHITE, true)); - FLogger.Append(ELog.Warning, () => - FLogger.Text($"Mappings must match '{manifest.BuildVersion}' in order to avoid errors", Constants.WHITE, true)); break; } case "ValorantLive": @@ -280,6 +283,9 @@ public class CUE4ParseViewModel : ViewModel break; case DefaultFileProvider d: d.Initialize(); + + var buildInfoPath = Path.Combine(UserSettings.Default.GameDirectory, "..\\..\\..\\Cloud\\BuildInfo.ini"); + if (File.Exists(buildInfoPath)) BuildInfo.Read(new StringReader(File.ReadAllText(buildInfoPath))); break; } @@ -429,108 +435,133 @@ public class CUE4ParseViewModel : ViewModel } private bool _cvaVerifDone { get; set; } - public void VerifyConsoleVariables() + public Task VerifyConsoleVariables() { - if (_cvaVerifDone) return; - _cvaVerifDone = true; + if (_cvaVerifDone) + return Task.CompletedTask; - var inst = new List(); - Provider.DefaultEngine.FindPropertyInstructions("ConsoleVariables", "a.StripAdditiveRefPose", inst); - if (inst.Count > 0 && inst[0].Value.Equals("1")) + return Task.Run(() => { - FLogger.Append(ELog.Warning, () => - FLogger.Text("Additive animations have their reference pose stripped, which will lead to inaccurate preview and export", Constants.WHITE, true)); - } + var inst = new List(); + Provider.DefaultEngine.FindPropertyInstructions("ConsoleVariables", "a.StripAdditiveRefPose", inst); + if (inst.Count > 0 && inst[0].Value.Equals("1")) + { + FLogger.Append(ELog.Warning, () => + FLogger.Text("Additive animations have their reference pose stripped, which will lead to inaccurate preview and export", Constants.WHITE, true)); + } + _cvaVerifDone = true; + }); } private int _vfcCount { get; set; } - public void VerifyVirtualCache() + public Task VerifyVirtualCache() { - if (_vfcCount > 0) return; - - _vfcCount = Provider.LoadVirtualCache(); if (_vfcCount > 0) - FLogger.Append(ELog.Information, - () => FLogger.Text($"{_vfcCount} cached packages loaded", Constants.WHITE, true)); + return Task.CompletedTask; + + return Task.Run(() => + { + _vfcCount = Provider.LoadVirtualCache(); + if (_vfcCount > 0) + FLogger.Append(ELog.Information, + () => FLogger.Text($"{_vfcCount} cached packages loaded", Constants.WHITE, true)); + }); } - public void VerifyContentBuildManifest() + public Task VerifyContentBuildManifest() { - if (!Provider.GameName.Equals("fortnitegame", StringComparison.OrdinalIgnoreCase)) return; + if (!Provider.GameName.Equals("fortnitegame", StringComparison.OrdinalIgnoreCase)) + return Task.CompletedTask; - var persistentDownloadDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FortniteGame/Saved/PersistentDownloadDir"); - if (!Directory.Exists(persistentDownloadDir)) return; - - var cachedManifest = new DirectoryInfo(Path.Combine(persistentDownloadDir, "ManifestCache")).GetFiles("*.manifest"); - if (cachedManifest.Length <= 0) - return; - - var manifest = new Manifest(File.ReadAllBytes(cachedManifest[0].FullName), new ManifestOptions + return Task.Run(() => { - ChunkBaseUri = new Uri("http://epicgames-download1.akamaized.net/Builds/Fortnite/Content/CloudDir/ChunksV4/", UriKind.Absolute), - ChunkCacheDirectory = Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")) - }); + var inst = new List(); + BuildInfo.FindPropertyInstructions("Content", "Label", inst); + if (inst.Count <= 0) return; - var onDemandFiles = new Dictionary(); - foreach (var fileManifest in manifest.FileManifests) - { - if (Provider.Files.TryGetValue(fileManifest.Name, out _)) continue; + var manifestInfo = _apiEndpointView.EpicApi.GetContentBuildManifest(default, inst[0].Value); + var manifestDir = new DirectoryInfo(Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "FortniteGame/Saved/PersistentDownloadDir/ManifestCache")); + var manifestPath = Path.Combine(manifestDir.FullName, manifestInfo?.FileName ?? ""); - var onDemandFile = new StreamedGameFile(fileManifest.Name, fileManifest.GetStream(), Provider.Versions); - if (Provider.IsCaseInsensitive) onDemandFiles[onDemandFile.Path.ToLowerInvariant()] = onDemandFile; - else onDemandFiles[onDemandFile.Path] = onDemandFile; - } + byte[] manifestData; + if (File.Exists(manifestPath)) + { + manifestData = File.ReadAllBytes(manifestPath); + } + else if (manifestInfo != null) + { + manifestData = manifestInfo.DownloadManifestData(); + File.WriteAllBytes(manifestPath, manifestData); + } + else if (manifestDir.Exists && manifestDir.GetFiles("*.manifest") is { Length: > 0} cachedManifests) + { + manifestData = File.ReadAllBytes(cachedManifests[0].FullName); + } + else return; - (Provider.Files as FileProviderDictionary)?.AddFiles(onDemandFiles); - if (onDemandFiles.Count > 0) - FLogger.Append(ELog.Information, - () => FLogger.Text($"{onDemandFiles.Count} streamed packages loaded", Constants.WHITE, true)); -#if DEBUG + var manifest = new Manifest(manifestData, new ManifestOptions + { + ChunkBaseUri = new Uri("http://epicgames-download1.akamaized.net/Builds/Fortnite/Content/CloudDir/ChunksV4/", UriKind.Absolute), + ChunkCacheDirectory = Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")) + }); - var missing = manifest.FileManifests.Count - onDemandFiles.Count; - if (missing != _vfcCount) // false positive if Provider.LoadVirtualCache takes too much time??? - FLogger.Append(ELog.Warning, - () => FLogger.Text($"{missing} packages went missing while loading VFC & CBM", Constants.WHITE, true)); -#endif + var onDemandFiles = new Dictionary(); + foreach (var fileManifest in manifest.FileManifests) + { + if (Provider.Files.TryGetValue(fileManifest.Name, out _)) continue; + + var onDemandFile = new StreamedGameFile(fileManifest.Name, fileManifest.GetStream(), Provider.Versions); + if (Provider.IsCaseInsensitive) onDemandFiles[onDemandFile.Path.ToLowerInvariant()] = onDemandFile; + else onDemandFiles[onDemandFile.Path] = onDemandFile; + } + + (Provider.Files as FileProviderDictionary)?.AddFiles(onDemandFiles); + if (onDemandFiles.Count > 0) + FLogger.Append(ELog.Information, + () => FLogger.Text($"{onDemandFiles.Count} streamed packages loaded", Constants.WHITE, true)); + #if DEBUG + + var missing = manifest.FileManifests.Count - onDemandFiles.Count; + if (missing != _vfcCount) // false positive if Provider.LoadVirtualCache takes too much time??? + FLogger.Append(ELog.Warning, + () => FLogger.Text($"{missing} packages went missing while loading VFC & CBM", Constants.WHITE, true)); + #endif + }); } public int LocalizedResourcesCount { get; set; } + public bool LocalResourcesDone { get; set; } + public bool HotfixedResourcesDone { get; set; } public async Task LoadLocalizedResources() { + var snapshot = LocalizedResourcesCount; await Task.WhenAll(LoadGameLocalizedResources(), LoadHotfixedLocalizedResources()).ConfigureAwait(false); - if (LocalizedResourcesCount > 0) + if (snapshot != LocalizedResourcesCount) { FLogger.Append(ELog.Information, () => FLogger.Text($"{LocalizedResourcesCount} localized resources loaded for '{UserSettings.Default.AssetLanguage.GetDescription()}'", Constants.WHITE, true)); } - else - { - FLogger.Append(ELog.Warning, () => - FLogger.Text($"Could not load localized resources in '{UserSettings.Default.AssetLanguage.GetDescription()}', language may not exist", Constants.WHITE, true)); - } } - - private bool _localResourcesDone { get; set; } private Task LoadGameLocalizedResources() { - if (_localResourcesDone) return Task.CompletedTask; + if (LocalResourcesDone) return Task.CompletedTask; return Task.Run(() => { LocalizedResourcesCount += Provider.LoadLocalization(UserSettings.Default.AssetLanguage); - _localResourcesDone = true; + LocalResourcesDone = true; }); } - - private bool _hotfixedResourcesDone { get; set; } private Task LoadHotfixedLocalizedResources() { - if (Game != FGame.FortniteGame || _hotfixedResourcesDone) return Task.CompletedTask; + if (!Provider.GameName.Equals("fortnitegame", StringComparison.OrdinalIgnoreCase) || HotfixedResourcesDone) return Task.CompletedTask; return Task.Run(() => { var hotfixes = ApplicationService.ApiEndpointView.CentralApi.GetHotfixes(default, Provider.GetLanguageCode(UserSettings.Default.AssetLanguage)); if (hotfixes == null) return; - _hotfixedResourcesDone = true; + HotfixedResourcesDone = true; foreach (var entries in hotfixes) { if (!Provider.LocalizedResources.ContainsKey(entries.Key)) diff --git a/FModel/Views/SettingsView.xaml.cs b/FModel/Views/SettingsView.xaml.cs index 2cdd3854..e48d1497 100644 --- a/FModel/Views/SettingsView.xaml.cs +++ b/FModel/Views/SettingsView.xaml.cs @@ -49,6 +49,8 @@ public partial class SettingsView { case SettingsOut.ReloadLocres: _applicationView.CUE4Parse.LocalizedResourcesCount = 0; + _applicationView.CUE4Parse.LocalResourcesDone = false; + _applicationView.CUE4Parse.HotfixedResourcesDone = false; await _applicationView.CUE4Parse.LoadLocalizedResources(); break; case SettingsOut.ReloadMappings: