From 4be7d4c3fb40aa00b3779c4fc5a56384bd7fbe92 Mon Sep 17 00:00:00 2001 From: Marlon Date: Thu, 4 Apr 2024 06:18:49 +0200 Subject: [PATCH] small rework removed DotNetZip make use of Oodle.NET & Zlib-ng.NET updated EpicManifestParser minor optimizations --- CUE4Parse | 2 +- FModel/FModel.csproj | 12 +- FModel/FModel.sln | 6 - FModel/Helper.cs | 44 ++++---- FModel/MainWindow.xaml.cs | 2 + .../ApiEndpoints/EpicApiEndpoint.cs | 8 +- .../ApiEndpoints/ValorantApiEndpoint.cs | 33 +++--- FModel/ViewModels/ApplicationViewModel.cs | 28 +++-- FModel/ViewModels/CUE4ParseViewModel.cs | 103 +++++++++++++----- 9 files changed, 150 insertions(+), 88 deletions(-) diff --git a/CUE4Parse b/CUE4Parse index 909fae9a..de075f49 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 909fae9ad24f8c9b0216af824b3c021dd39f1956 +Subproject commit de075f490ce4dc838a5175b78801d45c7bda1588 diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index 27458409..19873095 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -140,28 +140,28 @@ - + - - + + + - + - + - diff --git a/FModel/FModel.sln b/FModel/FModel.sln index 52492681..238d1b59 100644 --- a/FModel/FModel.sln +++ b/FModel/FModel.sln @@ -9,8 +9,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUE4Parse", "..\CUE4Parse\C EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUE4Parse-Conversion", "..\CUE4Parse\CUE4Parse-Conversion\CUE4Parse-Conversion.csproj", "{D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EpicManifestParser", "..\EpicManifestParser\src\EpicManifestParser\EpicManifestParser.csproj", "{D4958A8B-815B-421D-A988-2A4E8E2B582D}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -29,10 +27,6 @@ Global {D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}.Debug|Any CPU.Build.0 = Debug|Any CPU {D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}.Release|Any CPU.Build.0 = Release|Any CPU - {D4958A8B-815B-421D-A988-2A4E8E2B582D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D4958A8B-815B-421D-A988-2A4E8E2B582D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D4958A8B-815B-421D-A988-2A4E8E2B582D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D4958A8B-815B-421D-A988-2A4E8E2B582D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/FModel/Helper.cs b/FModel/Helper.cs index 67cd659f..c7a43c50 100644 --- a/FModel/Helper.cs +++ b/FModel/Helper.cs @@ -1,30 +1,32 @@ -using System; +using System; using System.Linq; -using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; using System.Windows; namespace FModel; public static class Helper { - [StructLayout(LayoutKind.Explicit)] - private struct NanUnion - { - [FieldOffset(0)] - internal double DoubleValue; - [FieldOffset(0)] - internal readonly ulong UlongValue; - } - public static string FixKey(string key) { if (string.IsNullOrEmpty(key)) return string.Empty; - if (key.StartsWith("0x")) - key = key[2..]; + var keySpan = key.AsSpan().Trim(); + if (keySpan.Length > sizeof(char) * (2 /* 0x */ + 32 /* FAES = 256 bit */)) // maybe strictly check for length? + return string.Empty; // bullshit key - return "0x" + key.ToUpper().Trim(); + Span resultSpan = stackalloc char[keySpan.Length + 2 /* pad for 0x */]; + keySpan.ToUpperInvariant(resultSpan[2..]); + + if (resultSpan[2..].StartsWith("0X")) + resultSpan = resultSpan[2..]; + else + resultSpan[0] = '0'; + + resultSpan[1] = 'x'; + + return new string(resultSpan); } public static void OpenWindow(string windowName, Action action) where T : Window @@ -74,9 +76,9 @@ public static class Helper public static bool IsNaN(double value) { - var t = new NanUnion { DoubleValue = value }; - var exp = t.UlongValue & 0xfff0000000000000; - var man = t.UlongValue & 0x000fffffffffffff; + var ulongValue = Unsafe.As(ref value); + var exp = ulongValue & 0xfff0000000000000; + var man = ulongValue & 0x000fffffffffffff; return exp is 0x7ff0000000000000 or 0xfff0000000000000 && man != 0; } @@ -96,13 +98,17 @@ public static class Helper return -d < n && d > n; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float DegreesToRadians(float degrees) { - return MathF.PI / 180f * degrees; + const float ratio = MathF.PI / 180f; + return ratio * degrees; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float RadiansToDegrees(float radians) { - return radians* 180f / MathF.PI; + const float ratio = 180f / MathF.PI; + return radians * ratio; } } diff --git a/FModel/MainWindow.xaml.cs b/FModel/MainWindow.xaml.cs index 86212096..24d66a1a 100644 --- a/FModel/MainWindow.xaml.cs +++ b/FModel/MainWindow.xaml.cs @@ -61,6 +61,8 @@ public partial class MainWindow break; } + await _applicationView.CUE4Parse.InitOodle(); + await _applicationView.CUE4Parse.InitZlib(); await _applicationView.CUE4Parse.Initialize(); await _applicationView.AesManager.InitAes(); await _applicationView.UpdateProvider(true); diff --git a/FModel/ViewModels/ApiEndpoints/EpicApiEndpoint.cs b/FModel/ViewModels/ApiEndpoints/EpicApiEndpoint.cs index 386762ce..6b1eeea2 100644 --- a/FModel/ViewModels/ApiEndpoints/EpicApiEndpoint.cs +++ b/FModel/ViewModels/ApiEndpoints/EpicApiEndpoint.cs @@ -1,10 +1,14 @@ using System.Threading; using System.Threading.Tasks; -using EpicManifestParser.Objects; + +using EpicManifestParser.Api; + using FModel.Framework; using FModel.Settings; using FModel.ViewModels.ApiEndpoints.Models; + using RestSharp; + using Serilog; namespace FModel.ViewModels.ApiEndpoints; @@ -25,7 +29,7 @@ public class EpicApiEndpoint : AbstractApiProvider 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 response.IsSuccessful ? new ManifestInfo(response.Content) : null; + return response.IsSuccessful ? ManifestInfo.Deserialize(response.RawBytes) : null; } public ManifestInfo GetManifest(CancellationToken token) diff --git a/FModel/ViewModels/ApiEndpoints/ValorantApiEndpoint.cs b/FModel/ViewModels/ApiEndpoints/ValorantApiEndpoint.cs index 67c51f96..8c827867 100644 --- a/FModel/ViewModels/ApiEndpoints/ValorantApiEndpoint.cs +++ b/FModel/ViewModels/ApiEndpoints/ValorantApiEndpoint.cs @@ -1,8 +1,3 @@ -using CUE4Parse.UE4.Exceptions; -using CUE4Parse.UE4.Readers; -using FModel.Settings; -using Ionic.Zlib; -using RestSharp; using System; using System.Collections.Generic; using System.IO; @@ -13,7 +8,15 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; + +using CUE4Parse.Compression; +using CUE4Parse.UE4.Exceptions; +using CUE4Parse.UE4.Readers; + using FModel.Framework; +using FModel.Settings; + +using RestSharp; namespace FModel.ViewModels.ApiEndpoints; @@ -40,26 +43,22 @@ public class VManifest public readonly VChunk[] Chunks; public readonly VPak[] Paks; - public VManifest(byte[] data) : this(new FByteArchive("CompressedValorantManifest", data)) - { - } - + public VManifest(byte[] data) : this(new FByteArchive("CompressedValorantManifest", data)) { } private VManifest(FArchive Ar) { using (Ar) { Header = new VHeader(Ar); var compressedBuffer = Ar.ReadBytes((int) Header.CompressedSize); - var uncompressedBuffer = ZlibStream.UncompressBuffer(compressedBuffer); - if (uncompressedBuffer.Length != Header.UncompressedSize) - throw new ParserException(Ar, $"Decompression failed, {uncompressedBuffer.Length} != {Header.UncompressedSize}"); + var uncompressedBuffer = new byte[(int)Header.UncompressedSize]; + ZlibHelper.Decompress(compressedBuffer, 0, compressedBuffer.Length, uncompressedBuffer, 0, uncompressedBuffer.Length); - using var manifest = new FByteArchive("UncompressedValorantManifest", uncompressedBuffer); - Chunks = manifest.ReadArray((int) Header.ChunkCount); - Paks = manifest.ReadArray((int) Header.PakCount, () => new VPak(manifest)); + var manifestAr = new FByteArchive("UncompressedValorantManifest", uncompressedBuffer); + Chunks = manifestAr.ReadArray((int) Header.ChunkCount); + Paks = manifestAr.ReadArray((int) Header.PakCount, () => new VPak(manifestAr)); - if (manifest.Position != manifest.Length) - throw new ParserException(manifest, $"Parsing failed, {manifest.Position} != {manifest.Length}"); + if (manifestAr.Position != manifestAr.Length) + throw new ParserException(manifestAr, $"Parsing failed, {manifestAr.Position} != {manifestAr.Length}"); } _client = new HttpClient(new HttpClientHandler diff --git a/FModel/ViewModels/ApplicationViewModel.cs b/FModel/ViewModels/ApplicationViewModel.cs index 6398f37a..fcfa725b 100644 --- a/FModel/ViewModels/ApplicationViewModel.cs +++ b/FModel/ViewModels/ApplicationViewModel.cs @@ -1,3 +1,10 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Threading.Tasks; +using System.Windows; + using FModel.Extensions; using FModel.Framework; using FModel.Services; @@ -5,12 +12,7 @@ using FModel.Settings; using FModel.ViewModels.Commands; using FModel.Views; using FModel.Views.Resources.Controls; -using Ionic.Zip; -using System; -using System.Diagnostics; -using System.IO; -using System.Threading.Tasks; -using System.Windows; + using MessageBox = AdonisUI.Controls.MessageBox; using MessageBoxButton = AdonisUI.Controls.MessageBoxButton; using MessageBoxImage = AdonisUI.Controls.MessageBoxImage; @@ -168,9 +170,17 @@ public class ApplicationViewModel : ViewModel await ApplicationService.ApiEndpointView.DownloadFileAsync("https://github.com/vgmstream/vgmstream/releases/latest/download/vgmstream-win.zip", vgmZipFilePath); if (new FileInfo(vgmZipFilePath).Length > 0) { - var zip = ZipFile.Read(vgmZipFilePath); - var zipDir = vgmZipFilePath.SubstringBeforeLast("\\"); - foreach (var e in zip) e.Extract(zipDir, ExtractExistingFileAction.OverwriteSilently); + var zipDir = Path.GetDirectoryName(vgmZipFilePath)!; + await using var zipFs = File.OpenRead(vgmZipFilePath); + using var zip = new ZipArchive(zipFs, ZipArchiveMode.Read); + + foreach (var entry in zip.Entries) + { + var entryPath = Path.Combine(zipDir, entry.FullName); + await using var entryFs = File.OpenRead(entryPath); + await using var entryStream = entry.Open(); + await entryStream.CopyToAsync(entryFs); + } } else { diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 977e83ba..90008f9d 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Http.Headers; @@ -7,7 +8,9 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Windows; + using AdonisUI.Controls; +using CUE4Parse.Compression; using CUE4Parse.Encryption.Aes; using CUE4Parse.FileProvider; using CUE4Parse.FileProvider.Vfs; @@ -17,23 +20,26 @@ using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Exports.Animation; using CUE4Parse.UE4.Assets.Exports.Material; using CUE4Parse.UE4.Assets.Exports.SkeletalMesh; -using CUE4Parse.UE4.Assets.Exports.Verse; using CUE4Parse.UE4.Assets.Exports.Sound; using CUE4Parse.UE4.Assets.Exports.StaticMesh; using CUE4Parse.UE4.Assets.Exports.Texture; +using CUE4Parse.UE4.Assets.Exports.Verse; using CUE4Parse.UE4.Assets.Exports.Wwise; using CUE4Parse.UE4.IO; using CUE4Parse.UE4.Localization; +using CUE4Parse.UE4.Objects.Core.Serialization; using CUE4Parse.UE4.Objects.Engine; using CUE4Parse.UE4.Oodle.Objects; using CUE4Parse.UE4.Readers; using CUE4Parse.UE4.Shaders; using CUE4Parse.UE4.Versions; using CUE4Parse.UE4.Wwise; + using CUE4Parse_Conversion; using CUE4Parse_Conversion.Sounds; -using CUE4Parse.UE4.Objects.Core.Serialization; -using EpicManifestParser.Objects; + +using EpicManifestParser; + using FModel.Creator; using FModel.Extensions; using FModel.Framework; @@ -42,13 +48,20 @@ using FModel.Settings; using FModel.Views; using FModel.Views.Resources.Controls; using FModel.Views.Snooper; + using Newtonsoft.Json; + using Ookii.Dialogs.Wpf; + using OpenTK.Windowing.Common; using OpenTK.Windowing.Desktop; + using Serilog; + using SkiaSharp; + using UE4Config.Parsing; + using Application = System.Windows.Application; namespace FModel.ViewModels; @@ -190,45 +203,41 @@ public class CUE4ParseViewModel : ViewModel case "FortniteLive": { var manifestInfo = _apiEndpointView.EpicApi.GetManifest(cancellationToken); - if (manifestInfo == null) + if (manifestInfo is null) { - throw new Exception("Could not load latest Fortnite manifest, you may have to switch to your local installation."); + throw new FileLoadException("Could not load latest Fortnite manifest, you may have to switch to your local installation."); } - byte[] manifestData; - var chunksDir = Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")); - var manifestPath = Path.Combine(chunksDir.FullName, manifestInfo.FileName); - if (File.Exists(manifestPath)) + var cacheDir = Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")).FullName; + var manifestOptions = new ManifestParseOptions { - manifestData = File.ReadAllBytes(manifestPath); - } - else - { - manifestData = manifestInfo.DownloadManifestData(); - File.WriteAllBytes(manifestPath, manifestData); - } + ChunkCacheDirectory = cacheDir, + ManifestCacheDirectory = cacheDir, + ChunkBaseUrl = "http://epicgames-download1.akamaized.net/Builds/Fortnite/CloudDir/", + Zlibng = ZlibHelper.Instance + }; - var manifest = new Manifest(manifestData, new ManifestOptions - { - ChunkBaseUri = new Uri("http://epicgames-download1.akamaized.net/Builds/Fortnite/CloudDir/ChunksV4/", UriKind.Absolute), - ChunkCacheDirectory = chunksDir - }); + var startTs = Stopwatch.GetTimestamp(); + var (manifest, _) = manifestInfo.DownloadAndParseAsync(manifestOptions, + cancellationToken: cancellationToken).GetAwaiter().GetResult(); + var parseTime = Stopwatch.GetElapsedTime(startTs); + const bool cacheChunksAsIs = false; - foreach (var fileManifest in manifest.FileManifests) + foreach (var fileManifest in manifest.FileManifestList) { - if (fileManifest.Name.Equals("Cloud/IoStoreOnDemand.ini", StringComparison.OrdinalIgnoreCase)) + if (fileManifest.FileName.Equals("Cloud/IoStoreOnDemand.ini", StringComparison.OrdinalIgnoreCase)) { - IoStoreOnDemand.Read(new StreamReader(fileManifest.GetStream())); + IoStoreOnDemand.Read(new StreamReader(fileManifest.GetStream(cacheChunksAsIs))); continue; } - if (!_fnLive.IsMatch(fileManifest.Name)) continue; + if (!_fnLive.IsMatch(fileManifest.FileName)) continue; - p.RegisterVfs(fileManifest.Name, new Stream[] { fileManifest.GetStream() } - , it => new FStreamArchive(it, manifest.FileManifests.First(x => x.Name.Equals(it)).GetStream(), p.Versions)); + p.RegisterVfs(fileManifest.FileName, [ fileManifest.GetStream(cacheChunksAsIs) ] + , it => new FStreamArchive(it, manifest.FileManifestList.First(x => x.FileName.Equals(it)).GetStream(cacheChunksAsIs), p.Versions)); } FLogger.Append(ELog.Information, () => - FLogger.Text($"Fortnite has been loaded successfully in {manifest.ParseTime.TotalMilliseconds}ms", Constants.WHITE, true)); + FLogger.Text($"Fortnite [LIVE] has been loaded successfully in {parseTime.TotalMilliseconds}ms", Constants.WHITE, true)); break; } case "ValorantLive": @@ -351,6 +360,44 @@ public class CUE4ParseViewModel : ViewModel }); } + public async Task InitOodle() + { + var dataDir = Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")).FullName; + var oodlePath = Path.Combine(dataDir, OodleHelper.OODLE_DLL_NAME); + bool result; + if (File.Exists(OodleHelper.OODLE_DLL_NAME)) + { + File.Move(OodleHelper.OODLE_DLL_NAME, oodlePath, true); + result = true; + } + else + { + result = await OodleHelper.DownloadOodleDllAsync(oodlePath); + } + + OodleHelper.Initialize(oodlePath); + return result; + } + + public async Task InitZlib() + { + var dataDir = Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")).FullName; + var zlibPath = Path.Combine(dataDir, ZlibHelper.DLL_NAME); + bool result; + if (File.Exists(ZlibHelper.DLL_NAME)) + { + File.Move(ZlibHelper.DLL_NAME, zlibPath, true); + result = true; + } + else + { + result = await ZlibHelper.DownloadDllAsync(zlibPath); + } + + ZlibHelper.Initialize(zlibPath); + return result; + } + public Task InitMappings(bool force = false) { if (!UserSettings.IsEndpointValid(EEndpointType.Mapping, out var endpoint))