diff --git a/CUE4Parse b/CUE4Parse index 8067d493..897b1cce 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 8067d4938202e9211eaa4a2812c161601e8db3db +Subproject commit 897b1cce7c24298871780a979671a9b1b69453bd diff --git a/FModel/Creator/Bases/FN/BaseQuest.cs b/FModel/Creator/Bases/FN/BaseQuest.cs index ef3159aa..6d2e9b87 100644 --- a/FModel/Creator/Bases/FN/BaseQuest.cs +++ b/FModel/Creator/Bases/FN/BaseQuest.cs @@ -242,7 +242,8 @@ public class BaseQuest : BaseIcon { _informationPaint.TextSize = 25; _informationPaint.Typeface = Utils.Typefaces.Bundle; - Utils.DrawMultilineText(c, DisplayName, Width - padding, 0, SKTextAlign.Left, + + Utils.DrawMultilineText(c, Utils.RemoveHtmlTags(DisplayName).Replace(" ", " "), Width - padding, 0, SKTextAlign.Left, new SKRect(x, y + padding, maxX, Height - padding * 1.5f), _informationPaint, out _); } diff --git a/FModel/Enums.cs b/FModel/Enums.cs index c2f21f6e..8abb2e60 100644 --- a/FModel/Enums.cs +++ b/FModel/Enums.cs @@ -156,4 +156,6 @@ public enum EAssetCategory : uint SoundBank = Media + 4, AudioEvent = Media + 5, Particle = AssetCategoryExtensions.CategoryBase + (9 << 16), + GameSpecific = AssetCategoryExtensions.CategoryBase + (10 << 16), + Borderlands4 = GameSpecific + 1, } diff --git a/FModel/Framework/ImGuiController.cs b/FModel/Framework/ImGuiController.cs index 9181b4fb..b62fde21 100644 --- a/FModel/Framework/ImGuiController.cs +++ b/FModel/Framework/ImGuiController.cs @@ -66,9 +66,19 @@ public class ImGuiController : IDisposable var iniFileNamePtr = Marshal.StringToCoTaskMemUTF8(Path.Combine(UserSettings.Default.OutputDirectory, ".data", "imgui.ini")); io.NativePtr->IniFilename = (byte*)iniFileNamePtr; } - FontNormal = io.Fonts.AddFontFromFileTTF("C:\\Windows\\Fonts\\segoeui.ttf", 16 * DpiScale); - FontBold = io.Fonts.AddFontFromFileTTF("C:\\Windows\\Fonts\\segoeuib.ttf", 16 * DpiScale); - FontSemiBold = io.Fonts.AddFontFromFileTTF("C:\\Windows\\Fonts\\seguisb.ttf", 16 * DpiScale); + + // If not found, Fallback to default ImGui Font + var normalPath = @"C:\Windows\Fonts\segoeui.ttf"; + var boldPath = @"C:\Windows\Fonts\segoeuib.ttf"; + var semiBoldPath = @"C:\Windows\Fonts\seguisb.ttf"; + + if (File.Exists(normalPath)) + FontNormal = io.Fonts.AddFontFromFileTTF(normalPath, 16 * DpiScale); + if (File.Exists(boldPath)) + FontBold = io.Fonts.AddFontFromFileTTF(boldPath, 16 * DpiScale); + if (File.Exists(semiBoldPath)) + FontSemiBold = io.Fonts.AddFontFromFileTTF(semiBoldPath, 16 * DpiScale); + io.Fonts.AddFontDefault(); io.Fonts.Build(); // Build font atlas diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs index d3d83f3b..f6a77b19 100644 --- a/FModel/Settings/UserSettings.cs +++ b/FModel/Settings/UserSettings.cs @@ -266,6 +266,13 @@ namespace FModel.Settings set => SetProperty(ref _readShaderMaps, value); } + private bool _convertAudioOnBulkExport; + public bool ConvertAudioOnBulkExport + { + get => _convertAudioOnBulkExport; + set => SetProperty(ref _convertAudioOnBulkExport, value); + } + private IDictionary _perDirectory = new Dictionary(); public IDictionary PerDirectory { diff --git a/FModel/ViewModels/ApiEndpointViewModel.cs b/FModel/ViewModels/ApiEndpointViewModel.cs index 6276567a..249f450b 100644 --- a/FModel/ViewModels/ApiEndpointViewModel.cs +++ b/FModel/ViewModels/ApiEndpointViewModel.cs @@ -17,7 +17,7 @@ public class ApiEndpointViewModel public FortniteApiEndpoint FortniteApi { get; } public ValorantApiEndpoint ValorantApi { get; } - public FortniteCentralApiEndpoint CentralApi { get; } + public DillyApiEndpoint DillyApi { get; } public EpicApiEndpoint EpicApi { get; } public FModelApiEndpoint FModelApi { get; } public GitHubApiEndpoint GitHubApi { get; } @@ -27,7 +27,7 @@ public class ApiEndpointViewModel { FortniteApi = new FortniteApiEndpoint(_client); ValorantApi = new ValorantApiEndpoint(_client); - CentralApi = new FortniteCentralApiEndpoint(_client); + DillyApi = new DillyApiEndpoint(_client); EpicApi = new EpicApiEndpoint(_client); FModelApi = new FModelApiEndpoint(_client); GitHubApi = new GitHubApiEndpoint(_client); diff --git a/FModel/ViewModels/ApiEndpoints/FortniteCentralApiEndpoint.cs b/FModel/ViewModels/ApiEndpoints/DillyApiEndpoints.cs similarity index 52% rename from FModel/ViewModels/ApiEndpoints/FortniteCentralApiEndpoint.cs rename to FModel/ViewModels/ApiEndpoints/DillyApiEndpoints.cs index 0bb587bb..926061bc 100644 --- a/FModel/ViewModels/ApiEndpoints/FortniteCentralApiEndpoint.cs +++ b/FModel/ViewModels/ApiEndpoints/DillyApiEndpoints.cs @@ -2,18 +2,34 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using FModel.Framework; +using FModel.ViewModels.ApiEndpoints.Models; using RestSharp; using Serilog; namespace FModel.ViewModels.ApiEndpoints; -public class FortniteCentralApiEndpoint : AbstractApiProvider +public class DillyApiEndpoint : AbstractApiProvider { - public FortniteCentralApiEndpoint(RestClient client) : base(client) { } + private Backup[] _backups; + + public DillyApiEndpoint(RestClient client) : base(client) { } + + public async Task GetBackupsAsync(CancellationToken token) + { + var request = new FRestRequest($"https://export-service-new.dillyapis.com/v1/backups"); + 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.Data; + } + + public Backup[] GetBackups(CancellationToken token) + { + return _backups ??= GetBackupsAsync(token).GetAwaiter().GetResult(); + } public async Task>> GetHotfixesAsync(CancellationToken token, string language = "en") { - var request = new FRestRequest("https://fortnitecentral.genxgames.gg/api/v1/hotfixes") + var request = new FRestRequest("https://api.fortniteapi.com/v1/cloudstorage/hotfixes") { Interceptors = [_interceptor] }; diff --git a/FModel/ViewModels/ApiEndpoints/FModelApiEndpoint.cs b/FModel/ViewModels/ApiEndpoints/FModelApiEndpoint.cs index c12679bd..db2882ed 100644 --- a/FModel/ViewModels/ApiEndpoints/FModelApiEndpoint.cs +++ b/FModel/ViewModels/ApiEndpoints/FModelApiEndpoint.cs @@ -26,7 +26,6 @@ public class FModelApiEndpoint : AbstractApiProvider private News _news; private Info _infos; private Donator[] _donators; - private Backup[] _backups; private Game _game; private readonly IDictionary _communityDesigns = new Dictionary(); private ApplicationViewModel _applicationView => ApplicationService.ApplicationView; @@ -60,19 +59,6 @@ public class FModelApiEndpoint : AbstractApiProvider return _donators ??= GetDonatorsAsync().GetAwaiter().GetResult(); } - public async Task GetBackupsAsync(CancellationToken token, string gameName) - { - var request = new FRestRequest($"https://api.fmodel.app/v1/backups/{gameName}"); - 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.Data; - } - - public Backup[] GetBackups(CancellationToken token, string gameName) - { - return _backups ??= GetBackupsAsync(token, gameName).GetAwaiter().GetResult(); - } - public async Task GetGamesAsync(CancellationToken token, string gameName) { var request = new FRestRequest($"https://api.fmodel.app/v1/games/{gameName}"); diff --git a/FModel/ViewModels/ApiEndpoints/Models/FModelResponse.cs b/FModel/ViewModels/ApiEndpoints/Models/FModelResponse.cs index 6c9b8dc4..67c6f6a4 100644 --- a/FModel/ViewModels/ApiEndpoints/Models/FModelResponse.cs +++ b/FModel/ViewModels/ApiEndpoints/Models/FModelResponse.cs @@ -19,10 +19,8 @@ public class News [DebuggerDisplay("{" + nameof(FileName) + "}")] public class Backup { - [J] public string GameName { get; private set; } [J] public string FileName { get; private set; } - [J] public string DownloadUrl { get; private set; } - [J] public long FileSize { get; private set; } + [J] public string Url { get; private set; } } public class Donator diff --git a/FModel/ViewModels/ApplicationViewModel.cs b/FModel/ViewModels/ApplicationViewModel.cs index 0c8839b2..7822aa70 100644 --- a/FModel/ViewModels/ApplicationViewModel.cs +++ b/FModel/ViewModels/ApplicationViewModel.cs @@ -268,31 +268,15 @@ public class ApplicationViewModel : ViewModel public static async Task InitOodle() { - if (File.Exists(OodleHelper.OODLE_DLL_NAME_OLD)) - { - try - { - File.Delete(OodleHelper.OODLE_DLL_NAME_OLD); - } - catch { /* ignored */} - } - - var oodlePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", OodleHelper.OODLE_DLL_NAME_OLD); + var oodlePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", OodleHelper.OODLE_NAME_OLD); if (!File.Exists(oodlePath)) { - oodlePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", OodleHelper.OODLE_DLL_NAME); - } - - if (!File.Exists(oodlePath)) - { - if (!await OodleHelper.DownloadOodleDllAsync(oodlePath)) - { - FLogger.Append(ELog.Error, () => FLogger.Text("Failed to download Oodle", Constants.WHITE, true)); - return; - } + oodlePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", OodleHelper.OODLE_NAME_CURRENT); } OodleHelper.Initialize(oodlePath); + if (OodleHelper.Instance is null) + FLogger.Append(ELog.Error, () => FLogger.Text("Failed to download Oodle", Constants.WHITE, true)); } public static async Task InitZlib() diff --git a/FModel/ViewModels/AudioPlayerViewModel.cs b/FModel/ViewModels/AudioPlayerViewModel.cs index 9ffada58..d12b2e7b 100644 --- a/FModel/ViewModels/AudioPlayerViewModel.cs +++ b/FModel/ViewModels/AudioPlayerViewModel.cs @@ -617,6 +617,16 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable _ => throw new NotSupportedException() }; + if (wavData.Length is 0) + { + if (TryConvert(out var wavFilePathFallback)) + { + var newAudioFallback = new AudioFile(SelectedAudioFile.Id, new FileInfo(wavFilePathFallback)); + Replace(newAudioFallback); + return true; + } + } + string wavFilePath = Path.Combine( UserSettings.Default.AudioDirectory, SelectedAudioFile.FilePath.TrimStart('/')); @@ -645,7 +655,7 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable } private bool TryConvert(out string wavFilePath) => TryConvert(SelectedAudioFile.FilePath, SelectedAudioFile.Data, out wavFilePath); - private bool TryConvert(string inputFilePath, byte[] inputFileData, out string wavFilePath) + public static bool TryConvert(string inputFilePath, byte[] inputFileData, out string wavFilePath) { wavFilePath = string.Empty; var vgmFilePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "test.exe"); diff --git a/FModel/ViewModels/BackupManagerViewModel.cs b/FModel/ViewModels/BackupManagerViewModel.cs index 0010b761..ce2c0356 100644 --- a/FModel/ViewModels/BackupManagerViewModel.cs +++ b/FModel/ViewModels/BackupManagerViewModel.cs @@ -48,13 +48,13 @@ public class BackupManagerViewModel : ViewModel { await _threadWorkerView.Begin(cancellationToken => { - var backups = _apiEndpointView.FModelApi.GetBackups(cancellationToken, _gameName); + var backups = _apiEndpointView.DillyApi.GetBackups(cancellationToken); if (backups == null) return; Application.Current.Dispatcher.Invoke(() => { foreach (var backup in backups) Backups.Add(backup); - SelectedBackup = Backups.LastOrDefault(); + SelectedBackup = Backups.FirstOrDefault(); }); }); } @@ -93,7 +93,7 @@ public class BackupManagerViewModel : ViewModel await _threadWorkerView.Begin(_ => { var fullPath = Path.Combine(Path.Combine(UserSettings.Default.OutputDirectory, "Backups"), SelectedBackup.FileName); - _apiEndpointView.DownloadFile(SelectedBackup.DownloadUrl, fullPath); + _apiEndpointView.DownloadFile(SelectedBackup.Url, fullPath); SaveCheck(fullPath, SelectedBackup.FileName, "downloaded", "download"); }); } diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 45e0dad5..18e74bab 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -20,9 +20,12 @@ using CUE4Parse.FileProvider; using CUE4Parse.FileProvider.Objects; using CUE4Parse.FileProvider.Vfs; using CUE4Parse.GameTypes.Aion2.Objects; +using CUE4Parse.GameTypes.AoC.Objects; using CUE4Parse.GameTypes.AshEchoes.FileProvider; using CUE4Parse.GameTypes.SMG.UE4.Assets.Exports.Wwise; using CUE4Parse.GameTypes.KRD.Assets.Exports; +using CUE4Parse.GameTypes.Borderlands4.Assets.Exports; +using CUE4Parse.GameTypes.Borderlands4.Wwise; using CUE4Parse.MappingsProvider; using CUE4Parse.UE4.AssetRegistry; using CUE4Parse.UE4.Assets; @@ -526,7 +529,7 @@ public class CUE4ParseViewModel : ViewModel if (!Provider.ProjectName.Equals("fortnitegame", StringComparison.OrdinalIgnoreCase) || HotfixedResourcesDone) return Task.CompletedTask; return Task.Run(() => { - var hotfixes = ApplicationService.ApiEndpointView.CentralApi.GetHotfixes(CancellationToken.None, Provider.GetLanguageCode(UserSettings.Default.AssetLanguage)); + var hotfixes = ApplicationService.ApiEndpointView.DillyApi.GetHotfixes(CancellationToken.None, Provider.GetLanguageCode(UserSettings.Default.AssetLanguage)); if (hotfixes == null) return; Provider.Internationalization.Override(hotfixes); @@ -657,11 +660,16 @@ public class CUE4ParseViewModel : ViewModel break; } - case "dat" when Provider.ProjectName.Equals("Aion2", StringComparison.OrdinalIgnoreCase): - { - ProcessAion2DatFile(entry, updateUi, saveProperties); - break; - } + case "dat" when Provider.Versions.Game is EGame.GAME_Aion2: + { + ProcessAion2DatFile(entry, updateUi, saveProperties); + break; + } + case "dbc" when Provider.Versions.Game is EGame.GAME_AshesOfCreation: + { + ProcessCacheDBFile(entry, updateUi, saveProperties); + break; + } case "upluginmanifest": case "code-workspace": case "projectstore": @@ -690,6 +698,7 @@ public class CUE4ParseViewModel : ViewModel case "usda": case "ocio": case "data" when Provider.ProjectName is "OakGame": + case "scss": case "ini": case "txt": case "log": @@ -705,6 +714,7 @@ public class CUE4ParseViewModel : ViewModel case "css": case "csv": case "pem": + case "tsv": case "tps": case "tgc": // State of Decay 2 case "cpp": @@ -770,7 +780,7 @@ public class CUE4ParseViewModel : ViewModel case "bank": { var archive = entry.CreateReader(); - if (!FModProvider.TryLoadBank(archive, entry.NameWithoutExtension, out var fmodReader)) + if (!FmodProvider.TryLoadBank(archive, entry.NameWithoutExtension, out var fmodReader)) { Log.Error($"Failed to load FMOD bank {entry.Path}"); break; @@ -782,7 +792,7 @@ public class CUE4ParseViewModel : ViewModel var directory = Path.GetDirectoryName(entry.Path) ?? "/FMOD/Desktop/"; foreach (var sound in extractedSounds) { - SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio); + SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio, updateUi); } break; @@ -797,7 +807,7 @@ public class CUE4ParseViewModel : ViewModel var medias = WwiseProvider.ExtractBankSounds(wwise); foreach (var media in medias) { - SaveAndPlaySound(media.OutputPath, media.Extension, media.Data, saveAudio); + SaveAndPlaySound(media.OutputPath, media.Extension, media.Data, saveAudio, updateUi); } break; @@ -813,7 +823,7 @@ public class CUE4ParseViewModel : ViewModel var extractedSounds = CriWareProvider.ExtractCriWareSounds(awbReader, archive.Name); foreach (var sound in extractedSounds) { - SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio); + SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio, updateUi); } break; @@ -829,7 +839,7 @@ public class CUE4ParseViewModel : ViewModel var extractedSounds = CriWareProvider.ExtractCriWareSounds(acbReader, archive.Name); foreach (var sound in extractedSounds) { - SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio); + SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio, updateUi); } break; @@ -844,7 +854,7 @@ public class CUE4ParseViewModel : ViewModel // todo: CSCore.MediaFoundation.MediaFoundationException The byte stream type of the given URL is unsupported. case "aif": { var data = Provider.SaveAsset(entry); - SaveAndPlaySound(entry.PathWithoutExtension, entry.Extension, data, saveAudio); + SaveAndPlaySound(entry.PathWithoutExtension, entry.Extension, data, saveAudio, updateUi); break; } @@ -969,6 +979,30 @@ public class CUE4ParseViewModel : ViewModel TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(datfile, Formatting.Indented), saveProperties, updateUi); } } + + // Ashhes of Creation + void ProcessCacheDBFile(GameFile entry, bool updateUi, bool saveProperties) + { + var data = entry.Read(); + var dbc = new FAoCDBCReader(data, Provider.MappingsForGame, Provider.Versions); + for (var i = 0; i < dbc.Chunks.Length; i++) + { + if (!dbc.TryReadChunk(i, out var category, out var files)) + { + Log.Warning("Couldn't read {i} chuck in AoC CacheDB", i); + continue; + } + var fileName = Path.ChangeExtension(category, ".json"); + var directory = Path.Combine(UserSettings.Default.PropertiesDirectory, + UserSettings.Default.KeepDirectoryStructure ? entry.Directory : "", entry.Name, fileName).Replace('\\', '/'); + + Directory.CreateDirectory(directory.SubstringBeforeLast('/')); + + File.WriteAllText(directory, JsonConvert.SerializeObject(files, Formatting.Indented)); + } + + TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(dbc, Formatting.Indented), saveProperties, updateUi); + } } public void ExtractAndScroll(CancellationToken cancellationToken, string fullPath, string objectName, string parentExportType) @@ -1076,11 +1110,11 @@ public class CUE4ParseViewModel : ViewModel TabControl.SelectedTab.AddImage(sourceFile.SubstringAfterLast('/'), false, bitmap, false, updateUi); return false; } - // The Dark Pictures Anthology: House of Ashes + // Supermassive Games (for example - The Dark Pictures Anthology: House of Ashes etc.) case UExternalSource when (isNone || saveAudio) && pointer.Object.Value is UExternalSource externalSource: { var audioName = Path.GetFileNameWithoutExtension(externalSource.ExternalSourcePath); - SaveAndPlaySound(audioName, "wem", externalSource.Data?.WemFile ?? [], saveAudio); + SaveAndPlaySound(audioName, "wem", externalSource.Data?.WemFile ?? [], saveAudio, updateUi); return false; } case UAkAudioEvent when (isNone || saveAudio) && pointer.Object.Value is UAkAudioEvent audioEvent: @@ -1088,7 +1122,7 @@ public class CUE4ParseViewModel : ViewModel var extractedSounds = WwiseProvider.ExtractAudioEventSounds(audioEvent); foreach (var sound in extractedSounds) { - SaveAndPlaySound(sound.OutputPath, sound.Extension, sound.Data, saveAudio); + SaveAndPlaySound(sound.OutputPath, sound.Extension, sound.Data, saveAudio, updateUi); } return false; } @@ -1098,7 +1132,7 @@ public class CUE4ParseViewModel : ViewModel var directory = Path.GetDirectoryName(fmodEvent.Owner?.Name) ?? "/FMOD/Desktop/"; foreach (var sound in extractedSounds) { - SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio); + SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio, updateUi); } return false; } @@ -1108,7 +1142,7 @@ public class CUE4ParseViewModel : ViewModel var directory = Path.GetDirectoryName(fmodBank.Owner?.Name) ?? "/FMOD/Desktop/"; foreach (var sound in extractedSounds) { - SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio); + SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio, updateUi); } return false; } @@ -1127,13 +1161,17 @@ public class CUE4ParseViewModel : ViewModel directory = Path.GetDirectoryName(atomObject.Owner.Provider.FixPath(directory)); foreach (var sound in extractedSounds) { - SaveAndPlaySound(Path.Combine(directory, sound.Name).Replace("\\", "/"), sound.Extension, sound.Data, saveAudio); + SaveAndPlaySound(Path.Combine(directory, sound.Name).Replace("\\", "/"), sound.Extension, sound.Data, saveAudio, updateUi); } return false; } case UAkMediaAssetData when isNone || saveAudio: case USoundWave when isNone || saveAudio: { + // If UAkMediaAsset exists in the same package it should be used to handle the audio instead (because it contains actual audio name) + if (pointer.Object.Value is UAkMediaAssetData dataObj && dataObj.Outer is UAkMediaAsset) + return false; + var shouldDecompress = UserSettings.Default.CompressedAudioMode == ECompressedAudio.PlayDecompressed; pointer.Object.Value.Decode(shouldDecompress, out var audioFormat, out var data); var hasAf = !string.IsNullOrEmpty(audioFormat); @@ -1143,7 +1181,68 @@ public class CUE4ParseViewModel : ViewModel return false; } - SaveAndPlaySound(TabControl.SelectedTab.Entry.PathWithoutExtension.Replace('\\', '/'), audioFormat, data, saveAudio); + SaveAndPlaySound(TabControl.SelectedTab.Entry.PathWithoutExtension.Replace('\\', '/'), audioFormat, data, saveAudio, updateUi); + return false; + } + case UAkMediaAsset when (isNone || saveAudio) && pointer.Object.Value is UAkMediaAsset akMediaAsset: + { + var audioName = akMediaAsset.MediaName; + if (akMediaAsset.CurrentMediaAssetData?.TryLoad(out var akMediaAssetData) is true) + { + var shouldDecompress = UserSettings.Default.CompressedAudioMode is ECompressedAudio.PlayDecompressed; + akMediaAssetData.Decode(shouldDecompress, out var audioFormat, out var data); + + SaveAndPlaySound(audioName, audioFormat, data, saveAudio, updateUi); + } + return false; + } + case UAkAudioEventData when (isNone || saveAudio) && pointer.Object.Value is UAkAudioEventData akAudioEventData: + { + var shouldDecompress = UserSettings.Default.CompressedAudioMode is ECompressedAudio.PlayDecompressed; + foreach (var mediaIndex in akAudioEventData.MediaList) + { + if (mediaIndex.TryLoad(out var akMediaAsset)) + { + if (akMediaAsset.CurrentMediaAssetData?.TryLoad(out var akMediaAssetData) is true) + { + var audioName = akMediaAsset.MediaName ?? $"{akAudioEventData.Outer.Name} ({akMediaAsset.ID})"; + akMediaAssetData.Decode(shouldDecompress, out var audioFormat, out var data); + + SaveAndPlaySound(audioName, audioFormat, data, saveAudio, updateUi); + } + } + } + return false; + } + // Borderlands 4 + case UFaceFXAnimSet when (isNone || saveAudio) && pointer.Object.Value is UFaceFXAnimSet faceFXAnimSet: + { + if (Provider.Versions.Game is not EGame.GAME_Borderlands4) + return false; + + foreach (var faceFXAnimData in faceFXAnimSet.FaceFXAnimDataList) + { + var extractedSounds = WwiseProvider.ExtractAudioEventBorderlands4(faceFXAnimData.ID.Name, false); + foreach (var sound in extractedSounds) + { + SaveAndPlaySound(sound.OutputPath, sound.Extension, sound.Data, saveAudio, updateUi); + } + } + + return false; + } + // Borderlands 4 + case UGbxGraphAsset when (isNone || saveAudio) && pointer.Object.Value is UGbxGraphAsset gbxGraphAsset: + { + foreach (var (eventName, useSoundTag) in GbxAudioUtil.GetAndClearEvents()) + { + var extractedSounds = WwiseProvider.ExtractAudioEventBorderlands4(eventName, useSoundTag); + foreach (var sound in extractedSounds) + { + SaveAndPlaySound(sound.OutputPath, sound.Extension, sound.Data, saveAudio, updateUi); + } + } + return false; } case UWorld when isNone && UserSettings.Default.PreviewWorlds: @@ -1280,7 +1379,7 @@ public class CUE4ParseViewModel : ViewModel TabControl.SelectedTab.SetDocumentText(cpp, false, false); } - private void SaveAndPlaySound(string fullPath, string ext, byte[] data, bool isBulk) + private void SaveAndPlaySound(string fullPath, string ext, byte[] data, bool isBulk, bool updateUi) { if (fullPath.StartsWith('/')) fullPath = fullPath[1..]; var savedAudioPath = Path.Combine(UserSettings.Default.AudioDirectory, @@ -1290,9 +1389,28 @@ public class CUE4ParseViewModel : ViewModel { Directory.CreateDirectory(savedAudioPath.SubstringBeforeLast('/')); using var stream = new FileStream(savedAudioPath, FileMode.Create, FileAccess.Write); - using var writer = new BinaryWriter(stream); - writer.Write(data); - writer.Flush(); + using (var writer = new BinaryWriter(stream)) + { + writer.Write(data); + writer.Flush(); + } + + if (UserSettings.Default.ConvertAudioOnBulkExport) + { + AudioPlayerViewModel.TryConvert(savedAudioPath, data, out string wavFilePath); + savedAudioPath = wavFilePath; + } + + Log.Information("Successfully saved {FilePath}", savedAudioPath); + if (updateUi) + { + FLogger.Append(ELog.Information, () => + { + FLogger.Text("Successfully saved ", Constants.WHITE); + FLogger.Link(Path.GetFileName(savedAudioPath), savedAudioPath, true); + }); + } + return; } diff --git a/FModel/ViewModels/Commands/MenuCommand.cs b/FModel/ViewModels/Commands/MenuCommand.cs index 1c3e7dbb..fe223f88 100644 --- a/FModel/ViewModels/Commands/MenuCommand.cs +++ b/FModel/ViewModels/Commands/MenuCommand.cs @@ -65,7 +65,7 @@ public class MenuCommand : ViewModelCommand Process.Start(new ProcessStartInfo { FileName = Constants.DISCORD_LINK, UseShellExecute = true }); break; case "ToolBox_Clear_Logs": - FLogger.Logger.Text = string.Empty; + FLogger.ClearLogs(); break; case "ToolBox_Open_Output_Directory": Process.Start(new ProcessStartInfo { FileName = UserSettings.Default.OutputDirectory, UseShellExecute = true }); diff --git a/FModel/ViewModels/GameFileViewModel.cs b/FModel/ViewModels/GameFileViewModel.cs index 3e97b075..afd53544 100644 --- a/FModel/ViewModels/GameFileViewModel.cs +++ b/FModel/ViewModels/GameFileViewModel.cs @@ -7,6 +7,7 @@ using System.Windows.Media; using System.Windows.Media.Imaging; using CUE4Parse.FileProvider.Objects; +using CUE4Parse.GameTypes.Borderlands4.Assets.Exports; using CUE4Parse.GameTypes.FN.Assets.Exports.DataAssets; using CUE4Parse.GameTypes.SMG.UE4.Assets.Exports.Wwise; using CUE4Parse.GameTypes.SMG.UE4.Assets.Objects; @@ -20,6 +21,7 @@ using CUE4Parse.UE4.Assets.Exports.CustomizableObject; using CUE4Parse.UE4.Assets.Exports.Engine; using CUE4Parse.UE4.Assets.Exports.Engine.Font; using CUE4Parse.UE4.Assets.Exports.Fmod; +using CUE4Parse.UE4.Assets.Exports.FMod; using CUE4Parse.UE4.Assets.Exports.Foliage; using CUE4Parse.UE4.Assets.Exports.Internationalization; using CUE4Parse.UE4.Assets.Exports.LevelSequence; @@ -42,6 +44,7 @@ using CUE4Parse.UE4.Objects.PhysicsEngine; using CUE4Parse.UE4.Objects.RigVM; using CUE4Parse.UE4.Objects.UObject; using CUE4Parse.UE4.Objects.UObject.Editor; +using CUE4Parse.UE4.Versions; using CUE4Parse.Utils; using CUE4Parse_Conversion.Textures; @@ -235,18 +238,28 @@ public class GameFileViewModel(GameFile asset) : ViewModel UObjectRedirector => (EAssetCategory.ObjectRedirector, EBulkType.None), UPhysicalMaterial => (EAssetCategory.PhysicalMaterial, EBulkType.None), - USoundAtomCue or UAkAudioEvent or USoundCue or UFMODEvent => (EAssetCategory.AudioEvent, EBulkType.Audio), + USoundAtomCue or UAkAudioEvent or USoundCue or UFMODEvent + or UAkAssetData or UAkAssetPlatformData => (EAssetCategory.AudioEvent, EBulkType.Audio), + + UFMODBankLookup => (EAssetCategory.Data, EBulkType.None), + + UFMODBus or UFMODSnapshot or UFMODSnapshotReverb or UFMODVCA => (EAssetCategory.Audio, EBulkType.None), UFMODBank or UAkAudioBank or UAtomWaveBank or UAkInitBank => (EAssetCategory.SoundBank, EBulkType.Audio), UWwiseAssetLibrary or USoundBase or UAkMediaAssetData or UAtomCueSheet - or USoundAtomCueSheet or UAkAudioType or UExternalSource or UExternalSourceBank => (EAssetCategory.Audio, EBulkType.Audio), + or USoundAtomCueSheet or UAkAudioType or UExternalSource or UExternalSourceBank + or UAkMediaAsset => (EAssetCategory.Audio, EBulkType.Audio), UFileMediaSource => (EAssetCategory.Video, EBulkType.None), UFont or UFontFace or USMGLocaleFontUMG => (EAssetCategory.Font, EBulkType.None), UNiagaraSystem or UNiagaraScriptBase or UParticleSystem => (EAssetCategory.Particle, EBulkType.None), + // Game specific assets below + UGbxGraphAsset => (EAssetCategory.Borderlands4, EBulkType.Audio), // Borderlands 4 + UFaceFXAnimSet when _applicationView.CUE4Parse?.Provider.Versions.Game is EGame.GAME_Borderlands4 => (EAssetCategory.Borderlands4, EBulkType.Audio), // Borderlands 4 + _ => (EAssetCategory.All, EBulkType.None), }; @@ -312,7 +325,8 @@ public class GameFileViewModel(GameFile asset) : ViewModel private Task ResolveByExtensionAsync(EResolveCompute resolve) { Resolved |= EResolveCompute.Preview; - switch (Asset.Extension) + var lowercaseExtension = Asset.Extension.ToLowerInvariant(); + switch (lowercaseExtension) { case "uproject": case "uefnproject": @@ -331,6 +345,12 @@ public class GameFileViewModel(GameFile asset) : ViewModel case "log": case "pem": case "xml": + case "gitignore": + case "html": + case "css": + case "js": + case "data": + case "csv": AssetCategory = EAssetCategory.Data; break; case "ushaderbytecode": @@ -379,7 +399,7 @@ public class GameFileViewModel(GameFile asset) : ViewModel stream.Position = 0; SKBitmap bitmap; - if (Asset.Extension == "svg") + if (lowercaseExtension == "svg") { var svg = new SKSvg(); svg.Load(stream); @@ -401,7 +421,7 @@ public class GameFileViewModel(GameFile asset) : ViewModel bitmap = SKBitmap.Decode(stream); } - using var image = bitmap.Encode(Asset.Extension == "jpg" ? SKEncodedImageFormat.Jpeg : SKEncodedImageFormat.Png, 100); + using var image = bitmap.Encode(lowercaseExtension == "jpg" ? SKEncodedImageFormat.Jpeg : SKEncodedImageFormat.Png, 100); SetPreviewImage(image); bitmap.Dispose(); diff --git a/FModel/Views/Resources/Colors.xaml b/FModel/Views/Resources/Colors.xaml index e2753c4d..cf0f2e86 100644 --- a/FModel/Views/Resources/Colors.xaml +++ b/FModel/Views/Resources/Colors.xaml @@ -42,4 +42,12 @@ + + + + + + + + diff --git a/FModel/Views/Resources/Controls/AvalonEditor.xaml.cs b/FModel/Views/Resources/Controls/AvalonEditor.xaml.cs index db81c600..9aebc30c 100644 --- a/FModel/Views/Resources/Controls/AvalonEditor.xaml.cs +++ b/FModel/Views/Resources/Controls/AvalonEditor.xaml.cs @@ -27,7 +27,7 @@ public partial class AvalonEditor private readonly Dictionary> _savedCarets = new(); private NavigationList _caretsOffsets { - get => MyAvalonEditor.Document != null + get => MyAvalonEditor.Document != null && MyAvalonEditor.Document.FileName != null ? _savedCarets.GetOrAdd(MyAvalonEditor.Document.FileName, () => new NavigationList()) : new NavigationList(); } diff --git a/FModel/Views/Resources/Controls/Rtb/CustomRichTextBox.cs b/FModel/Views/Resources/Controls/Rtb/CustomRichTextBox.cs index c480a4bf..8b995c01 100644 --- a/FModel/Views/Resources/Controls/Rtb/CustomRichTextBox.cs +++ b/FModel/Views/Resources/Controls/Rtb/CustomRichTextBox.cs @@ -155,6 +155,12 @@ public class FLogger : ITextFormatter { new TextRange(document.ContentStart, document.ContentEnd).Text = text; } + + public static void ClearLogs() + { + Logger.Document.Blocks.Clear(); + _previous = 0; + } } public class CustomRichTextBox : RichTextBox diff --git a/FModel/Views/Resources/Converters/FileToGeometryConverter.cs b/FModel/Views/Resources/Converters/FileToGeometryConverter.cs index 437eefe1..e8a605d9 100644 --- a/FModel/Views/Resources/Converters/FileToGeometryConverter.cs +++ b/FModel/Views/Resources/Converters/FileToGeometryConverter.cs @@ -79,11 +79,18 @@ public class FileToGeometryConverter : IMultiValueConverter "function" => ("FunctionIcon", "NeutralBrush"), "bin" => ("DataTableIcon", "BinaryBrush"), "xml" => ("XmlIcon", "JsonXmlBrush"), + "gitignore" => ("GitIcon", "GitBrush"), + "html" => ("HtmlIcon", "HtmlBrush"), + "js" => ("JavaScriptIcon", "JavaScriptBrush"), + "css" => ("CssIcon", "CssBrush"), + "csv" => ("CsvIcon", "CsvBrush"), _ => ("DataTableIcon", "NeutralBrush") }, EAssetCategory.ByteCode => ("CodeIcon", "CodeBrush"), + EAssetCategory.Borderlands4 => ("BorderlandsIcon", "BorderlandsBrush"), + _ => ("AssetIcon", "NeutralBrush") }; diff --git a/FModel/Views/Resources/Icons.xaml b/FModel/Views/Resources/Icons.xaml index 93b90cc7..909cefb7 100644 --- a/FModel/Views/Resources/Icons.xaml +++ b/FModel/Views/Resources/Icons.xaml @@ -92,4 +92,12 @@ M4,3C2.89,3 2,3.89 2,5V15A2,2 0 0,0 4,17H12V22L15,19L18,22V17H20A2,2 0 0,0 22,15V8L22,6V5A2,2 0 0,0 20,3H16V3H4M12,5L15,7L18,5V8.5L21,10L18,11.5V15L15,13L12,15V11.5L9,10L12,8.5V5M4,5H9V7H4V5M4,9H7V11H4V9M4,13H9V15H4V13Z M289.718,1208.22 L283.795,1202.28 C283.404,1201.89 282.768,1201.89 282.376,1202.28 C281.984,1202.68 282,1203.35 282,1204 L282,1207 L266,1207 L266,1204 C266,1203.35 266.016,1202.68 265.624,1202.28 C265.232,1201.89 264.597,1201.89 264.205,1202.28 L258.282,1208.22 C258.073,1208.43 257.983,1208.71 257.998,1208.98 C257.983,1209.26 258.073,1209.54 258.282,1209.75 L264.205,1215.69 C264.597,1216.08 265.232,1216.08 265.624,1215.69 C266.016,1215.29 266,1214.39 266,1214 L266,1211 L282,1211 L282,1214 C282,1214.65 281.984,1215.29 282.376,1215.69 C282.768,1216.08 283.404,1216.08 283.795,1215.69 L289.718,1209.75 C289.927,1209.54 290.017,1209.26 290.002,1208.98 C290.017,1208.71 289.927,1208.43 289.718,1208.22 M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z + M2.6,10.59L8.38,4.8L10.07,6.5C9.83,7.35 10.22,8.28 11,8.73V14.27C10.4,14.61 10,15.26 10,16A2,2 0 0,0 12,18A2,2 0 0,0 14,16C14,15.26 13.6,14.61 13,14.27V9.41L15.07,11.5C15,11.65 15,11.82 15,12A2,2 0 0,0 17,14A2,2 0 0,0 19,12A2,2 0 0,0 17,10C16.82,10 16.65,10 16.5,10.07L13.93,7.5C14.19,6.57 13.71,5.55 12.78,5.16C12.35,5 11.9,4.96 11.5,5.07L9.8,3.38L10.59,2.6C11.37,1.81 12.63,1.81 13.41,2.6L21.4,10.59C22.19,11.37 22.19,12.63 21.4,13.41L13.41,21.4C12.63,22.19 11.37,22.19 10.59,21.4L2.6,13.41C1.81,12.63 1.81,11.37 2.6,10.59Z + M12,17.56L16.07,16.43L16.62,10.33H9.38L9.2,8.3H16.8L17,6.31H7L7.56,12.32H14.45L14.22,14.9L12,15.5L9.78,14.9L9.64,13.24H7.64L7.93,16.43L12,17.56M4.07,3H19.93L18.5,19.2L12,21L5.5,19.2L4.07,3Z + M3,3H21V21H3V3M7.73,18.04C8.13,18.89 8.92,19.59 10.27,19.59C11.77,19.59 12.8,18.79 12.8,17.04V11.26H11.1V17C11.1,17.86 10.75,18.08 10.2,18.08C9.62,18.08 9.38,17.68 9.11,17.21L7.73,18.04M13.71,17.86C14.21,18.84 15.22,19.59 16.8,19.59C18.4,19.59 19.6,18.76 19.6,17.23C19.6,15.82 18.79,15.19 17.35,14.57L16.93,14.39C16.2,14.08 15.89,13.87 15.89,13.37C15.89,12.96 16.2,12.64 16.7,12.64C17.18,12.64 17.5,12.85 17.79,13.37L19.1,12.5C18.55,11.54 17.77,11.17 16.7,11.17C15.19,11.17 14.22,12.13 14.22,13.4C14.22,14.78 15.03,15.43 16.25,15.95L16.67,16.13C17.45,16.47 17.91,16.68 17.91,17.26C17.91,17.74 17.46,18.09 16.76,18.09C15.93,18.09 15.45,17.66 15.09,17.06L13.71,17.86Z + M5,3L4.35,6.34H17.94L17.5,8.5H3.92L3.26,11.83H16.85L16.09,15.64L10.61,17.45L5.86,15.64L6.19,14H2.85L2.06,18L9.91,21L18.96,18L20.16,11.97L20.4,10.76L21.94,3H5Z + M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2M15 16L13 20H10L12 16H9V11H15V16M13 9V3.5L18.5 9H13Z + + + M13,9V3.5L18.5,9M6,2C4.89,2 4,2.89 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6 ZM12,11A3,3 0 1,0 12,17A3,3 0 0,0 12,11 ZM12,12.5L14,16H13L12,14.5L11,16H10L12,12.5Z diff --git a/FModel/Views/SettingsView.xaml b/FModel/Views/SettingsView.xaml index 3869882d..450f97a3 100644 --- a/FModel/Views/SettingsView.xaml +++ b/FModel/Views/SettingsView.xaml @@ -43,6 +43,7 @@ + @@ -157,7 +158,8 @@ + IsChecked="{Binding KeepDirectoryStructure, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 0" + Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" /> @@ -207,7 +209,8 @@ + IsChecked="{Binding SettingsView.MappingEndpoint.Overwrite, Mode=TwoWay}" + Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" /> + IsChecked="{Binding ReadScriptData, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10" + Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" /> + IsChecked="{Binding ReadShaderMaps, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10" + Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" /> + IsChecked="{Binding ShowDecompileOption, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10" + Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" /> - - + + + + -