mirror of
https://github.com/4sval/FModel.git
synced 2026-04-26 00:04:53 -05:00
load ondemand archives
This commit is contained in:
parent
5ef205c142
commit
88adcd03be
2
.github/workflows/qa.yml
vendored
2
.github/workflows/qa.yml
vendored
|
|
@ -3,8 +3,6 @@ name: FModel QA Builder
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ dev ]
|
branches: [ dev ]
|
||||||
pull_request:
|
|
||||||
branches: [ dev ]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 3ff8c179dfbe817f22e9672dab9c2901a58b9db7
|
Subproject commit 5107809d35e3c43121c717b3b3aa732aa08e77fb
|
||||||
|
|
@ -69,8 +69,7 @@ public partial class MainWindow
|
||||||
#endif
|
#endif
|
||||||
await Task.WhenAll(
|
await Task.WhenAll(
|
||||||
_applicationView.CUE4Parse.VerifyConsoleVariables(),
|
_applicationView.CUE4Parse.VerifyConsoleVariables(),
|
||||||
_applicationView.CUE4Parse.VerifyVirtualCache(),
|
_applicationView.CUE4Parse.VerifyOnDemandArchives(),
|
||||||
_applicationView.CUE4Parse.VerifyContentBuildManifest(),
|
|
||||||
_applicationView.CUE4Parse.InitMappings(),
|
_applicationView.CUE4Parse.InitMappings(),
|
||||||
_applicationView.InitImGuiSettings(newOrUpdated),
|
_applicationView.InitImGuiSettings(newOrUpdated),
|
||||||
_applicationView.InitVgmStream(),
|
_applicationView.InitVgmStream(),
|
||||||
|
|
|
||||||
|
|
@ -14,20 +14,12 @@ public class EpicApiEndpoint : AbstractApiProvider
|
||||||
private const string _OAUTH_URL = "https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/token";
|
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 _BASIC_TOKEN = "basic MzQ0NmNkNzI2OTRjNGE0NDg1ZDgxYjc3YWRiYjIxNDE6OTIwOWQ0YTVlMjVhNDU3ZmI5YjA3NDg5ZDMxM2I0MWE=";
|
||||||
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 _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) { }
|
public EpicApiEndpoint(RestClient client) : base(client) { }
|
||||||
|
|
||||||
public async Task<ManifestInfo> GetManifestAsync(CancellationToken token)
|
public async Task<ManifestInfo> GetManifestAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
if (await IsExpired().ConfigureAwait(false))
|
await VerifyAuth(token).ConfigureAwait(false);
|
||||||
{
|
|
||||||
var auth = await GetAuthAsync(token).ConfigureAwait(false);
|
|
||||||
if (auth != null)
|
|
||||||
{
|
|
||||||
UserSettings.Default.LastAuthResponse = auth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = new FRestRequest(_APP_URL);
|
var request = new FRestRequest(_APP_URL);
|
||||||
request.AddHeader("Authorization", $"bearer {UserSettings.Default.LastAuthResponse.AccessToken}");
|
request.AddHeader("Authorization", $"bearer {UserSettings.Default.LastAuthResponse.AccessToken}");
|
||||||
|
|
@ -36,7 +28,12 @@ public class EpicApiEndpoint : AbstractApiProvider
|
||||||
return response.IsSuccessful ? new ManifestInfo(response.Content) : null;
|
return response.IsSuccessful ? new ManifestInfo(response.Content) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ContentBuildManifestInfo> GetContentBuildManifestAsync(CancellationToken token, string label)
|
public ManifestInfo GetManifest(CancellationToken token)
|
||||||
|
{
|
||||||
|
return GetManifestAsync(token).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task VerifyAuth(CancellationToken token)
|
||||||
{
|
{
|
||||||
if (await IsExpired().ConfigureAwait(false))
|
if (await IsExpired().ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
|
|
@ -46,23 +43,6 @@ public class EpicApiEndpoint : AbstractApiProvider
|
||||||
UserSettings.Default.LastAuthResponse = auth;
|
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)
|
|
||||||
{
|
|
||||||
return GetManifestAsync(token).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ContentBuildManifestInfo GetContentBuildManifest(CancellationToken token, string label)
|
|
||||||
{
|
|
||||||
return GetContentBuildManifestAsync(token, label).GetAwaiter().GetResult();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<AuthResponse> GetAuthAsync(CancellationToken token)
|
private async Task<AuthResponse> GetAuthAsync(CancellationToken token)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -31,7 +32,6 @@ using CUE4Parse.UE4.Versions;
|
||||||
using CUE4Parse.UE4.Wwise;
|
using CUE4Parse.UE4.Wwise;
|
||||||
using CUE4Parse_Conversion;
|
using CUE4Parse_Conversion;
|
||||||
using CUE4Parse_Conversion.Sounds;
|
using CUE4Parse_Conversion.Sounds;
|
||||||
using CUE4Parse.FileProvider.Objects;
|
|
||||||
using CUE4Parse.UE4.Objects.Core.Serialization;
|
using CUE4Parse.UE4.Objects.Core.Serialization;
|
||||||
using EpicManifestParser.Objects;
|
using EpicManifestParser.Objects;
|
||||||
using FModel.Creator;
|
using FModel.Creator;
|
||||||
|
|
@ -121,7 +121,7 @@ public class CUE4ParseViewModel : ViewModel
|
||||||
public AssetsFolderViewModel AssetsFolder { get; }
|
public AssetsFolderViewModel AssetsFolder { get; }
|
||||||
public SearchViewModel SearchVm { get; }
|
public SearchViewModel SearchVm { get; }
|
||||||
public TabControlViewModel TabControl { get; }
|
public TabControlViewModel TabControl { get; }
|
||||||
public ConfigIni BuildInfo { get; }
|
public ConfigIni IoStoreOnDemand { get; }
|
||||||
|
|
||||||
public CUE4ParseViewModel()
|
public CUE4ParseViewModel()
|
||||||
{
|
{
|
||||||
|
|
@ -175,7 +175,7 @@ public class CUE4ParseViewModel : ViewModel
|
||||||
AssetsFolder = new AssetsFolderViewModel();
|
AssetsFolder = new AssetsFolderViewModel();
|
||||||
SearchVm = new SearchViewModel();
|
SearchVm = new SearchViewModel();
|
||||||
TabControl = new TabControlViewModel();
|
TabControl = new TabControlViewModel();
|
||||||
BuildInfo = new ConfigIni(nameof(BuildInfo));
|
IoStoreOnDemand = new ConfigIni(nameof(IoStoreOnDemand));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Initialize()
|
public async Task Initialize()
|
||||||
|
|
@ -216,9 +216,9 @@ public class CUE4ParseViewModel : ViewModel
|
||||||
|
|
||||||
foreach (var fileManifest in manifest.FileManifests)
|
foreach (var fileManifest in manifest.FileManifests)
|
||||||
{
|
{
|
||||||
if (fileManifest.Name.Equals("Cloud/BuildInfo.ini", StringComparison.OrdinalIgnoreCase))
|
if (fileManifest.Name.Equals("Cloud/IoStoreOnDemand.ini", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
BuildInfo.Read(new StreamReader(fileManifest.GetStream()));
|
IoStoreOnDemand.Read(new StreamReader(fileManifest.GetStream()));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!_fnLive.IsMatch(fileManifest.Name)) continue;
|
if (!_fnLive.IsMatch(fileManifest.Name)) continue;
|
||||||
|
|
@ -252,8 +252,8 @@ public class CUE4ParseViewModel : ViewModel
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case DefaultFileProvider:
|
case DefaultFileProvider:
|
||||||
var buildInfoPath = Path.Combine(UserSettings.Default.GameDirectory, "..\\..\\..\\Cloud\\BuildInfo.ini");
|
var ioStoreOnDemandPath = Path.Combine(UserSettings.Default.GameDirectory, "..\\..\\..\\Cloud\\IoStoreOnDemand.ini");
|
||||||
if (File.Exists(buildInfoPath)) BuildInfo.Read(new StringReader(File.ReadAllText(buildInfoPath)));
|
if (File.Exists(ioStoreOnDemandPath)) IoStoreOnDemand.Read(new StringReader(File.ReadAllText(ioStoreOnDemandPath)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -294,7 +294,7 @@ public class CUE4ParseViewModel : ViewModel
|
||||||
if (Provider.MountedVfs.FirstOrDefault(x => x.Name == file.Name) is not { } vfs)
|
if (Provider.MountedVfs.FirstOrDefault(x => x.Name == file.Name) is not { } vfs)
|
||||||
{
|
{
|
||||||
if (Provider.UnloadedVfs.FirstOrDefault(x => x.Name == file.Name) is IoStoreReader store)
|
if (Provider.UnloadedVfs.FirstOrDefault(x => x.Name == file.Name) is IoStoreReader store)
|
||||||
file.FileCount = (int) store.Info.TocEntryCount - 1;
|
file.FileCount = (int) store.TocResource.Header.TocEntryCount - 1;
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -436,87 +436,35 @@ public class CUE4ParseViewModel : ViewModel
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private int _vfcCount { get; set; }
|
public Task VerifyOnDemandArchives()
|
||||||
public Task VerifyVirtualCache()
|
|
||||||
{
|
|
||||||
if (Provider is StreamedFileProvider { LiveGame: "FortniteLive" } || _vfcCount > 0)
|
|
||||||
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 Task VerifyContentBuildManifest()
|
|
||||||
{
|
{
|
||||||
|
// only local fortnite
|
||||||
if (Provider is not DefaultFileProvider || !Provider.InternalGameName.Equals("FortniteGame", StringComparison.OrdinalIgnoreCase))
|
if (Provider is not DefaultFileProvider || !Provider.InternalGameName.Equals("FortniteGame", StringComparison.OrdinalIgnoreCase))
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
// scuffed but working
|
||||||
var persistentDownloadDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FortniteGame/Saved/PersistentDownloadDir");
|
var persistentDownloadDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FortniteGame/Saved/PersistentDownloadDir");
|
||||||
var vfcMetadata = Path.Combine(persistentDownloadDir, "VFC", "vfc.meta");
|
var iasFileInfo = new FileInfo(Path.Combine(persistentDownloadDir, "ias", "ias.cache.0"));
|
||||||
if (!File.Exists(vfcMetadata))
|
if (!iasFileInfo.Exists || iasFileInfo.Length == 0)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
// load if local fortnite with ondemand disabled
|
return Task.Run(async () =>
|
||||||
// VFC folder is created at launch if ondemand
|
|
||||||
// VFC folder is deleted at launch if not ondemand anymore
|
|
||||||
return Task.Run(() =>
|
|
||||||
{
|
{
|
||||||
var inst = new List<InstructionToken>();
|
var inst = new List<InstructionToken>();
|
||||||
BuildInfo.FindPropertyInstructions("Content", "Label", inst);
|
IoStoreOnDemand.FindPropertyInstructions("Endpoint", "TocPath", inst);
|
||||||
if (inst.Count <= 0) return;
|
if (inst.Count <= 0) return;
|
||||||
|
|
||||||
var manifestInfo = _apiEndpointView.EpicApi.GetContentBuildManifest(default, inst[0].Value);
|
var ioStoreOnDemandPath = Path.Combine(UserSettings.Default.GameDirectory, "..\\..\\..\\Cloud", inst[0].Value.SubstringAfterLast("/").SubstringBefore("\""));
|
||||||
var manifestDir = new DirectoryInfo(Path.Combine(persistentDownloadDir, "ManifestCache"));
|
if (!File.Exists(ioStoreOnDemandPath)) return;
|
||||||
var manifestPath = Path.Combine(manifestDir.FullName, manifestInfo?.FileName ?? "");
|
|
||||||
|
|
||||||
byte[] manifestData;
|
await _apiEndpointView.EpicApi.VerifyAuth(default);
|
||||||
if (File.Exists(manifestPath))
|
await Provider.RegisterVfs(new IoChunkToc(ioStoreOnDemandPath), new IoStoreOnDemandOptions
|
||||||
{
|
{
|
||||||
manifestData = File.ReadAllBytes(manifestPath);
|
ChunkBaseUri = new Uri("https://download.epicgames.com/ias/fortnite/", UriKind.Absolute),
|
||||||
}
|
ChunkCacheDirectory = Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")),
|
||||||
else if (manifestInfo != null)
|
Authorization = new AuthenticationHeaderValue("Bearer", UserSettings.Default.LastAuthResponse.AccessToken)
|
||||||
{
|
|
||||||
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;
|
|
||||||
|
|
||||||
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"))
|
|
||||||
});
|
});
|
||||||
|
await Provider.MountAsync();
|
||||||
var onDemandFiles = new Dictionary<string, GameFile>();
|
|
||||||
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 > 0)
|
|
||||||
FLogger.Append(ELog.Debug,
|
|
||||||
() => FLogger.Text($"{missing} packages were already loaded by regular archives", Constants.WHITE, true));
|
|
||||||
#endif
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user