Merge branch 'dev' into dotnet-10

# Conflicts:
#	CUE4Parse
This commit is contained in:
Asval 2026-02-21 17:45:30 +01:00
commit 45896c3612
21 changed files with 289 additions and 90 deletions

@ -1 +1 @@
Subproject commit 8067d4938202e9211eaa4a2812c161601e8db3db
Subproject commit 897b1cce7c24298871780a979671a9b1b69453bd

View File

@ -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 _);
}

View File

@ -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,
}

View File

@ -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

View File

@ -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<string, DirectorySettings> _perDirectory = new Dictionary<string, DirectorySettings>();
public IDictionary<string, DirectorySettings> PerDirectory
{

View File

@ -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);

View File

@ -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<Backup[]> GetBackupsAsync(CancellationToken token)
{
var request = new FRestRequest($"https://export-service-new.dillyapis.com/v1/backups");
var response = await _client.ExecuteAsync<Backup[]>(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<IDictionary<string, IDictionary<string, string>>> 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]
};

View File

@ -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<string, CommunityDesign> _communityDesigns = new Dictionary<string, CommunityDesign>();
private ApplicationViewModel _applicationView => ApplicationService.ApplicationView;
@ -60,19 +59,6 @@ public class FModelApiEndpoint : AbstractApiProvider
return _donators ??= GetDonatorsAsync().GetAwaiter().GetResult();
}
public async Task<Backup[]> GetBackupsAsync(CancellationToken token, string gameName)
{
var request = new FRestRequest($"https://api.fmodel.app/v1/backups/{gameName}");
var response = await _client.ExecuteAsync<Backup[]>(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<Game> GetGamesAsync(CancellationToken token, string gameName)
{
var request = new FRestRequest($"https://api.fmodel.app/v1/games/{gameName}");

View File

@ -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

View File

@ -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()

View File

@ -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");

View File

@ -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");
});
}

View File

@ -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<UAkMediaAssetData>(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<UAkMediaAsset>(out var akMediaAsset))
{
if (akMediaAsset.CurrentMediaAssetData?.TryLoad<UAkMediaAssetData>(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;
}

View File

@ -65,7 +65,7 @@ public class MenuCommand : ViewModelCommand<ApplicationViewModel>
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 });

View File

@ -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();

View File

@ -42,4 +42,12 @@
<SolidColorBrush x:Key="LuaBrush" Color="DarkBlue" />
<SolidColorBrush x:Key="JsonXmlBrush" Color="LightGreen" />
<SolidColorBrush x:Key="CodeBrush" Color="SandyBrown" />
<SolidColorBrush x:Key="HtmlBrush" Color="Tomato" />
<SolidColorBrush x:Key="JavaScriptBrush" Color="Yellow" />
<SolidColorBrush x:Key="CssBrush" Color="MediumPurple" />
<SolidColorBrush x:Key="GitBrush" Color="Coral" />
<SolidColorBrush x:Key="CsvBrush" Color="ForestGreen" />
<!-- For specific games -->
<SolidColorBrush x:Key="BorderlandsBrush" Color="Yellow"></SolidColorBrush>
</ResourceDictionary>

View File

@ -27,7 +27,7 @@ public partial class AvalonEditor
private readonly Dictionary<string, NavigationList<int>> _savedCarets = new();
private NavigationList<int> _caretsOffsets
{
get => MyAvalonEditor.Document != null
get => MyAvalonEditor.Document != null && MyAvalonEditor.Document.FileName != null
? _savedCarets.GetOrAdd(MyAvalonEditor.Document.FileName, () => new NavigationList<int>())
: new NavigationList<int>();
}

View File

@ -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

View File

@ -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")
};

View File

@ -92,4 +92,12 @@
<Geometry x:Key="CertificateIcon">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</Geometry>
<Geometry x:Key="RedirectorIcon">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</Geometry>
<Geometry x:Key="CodeIcon">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</Geometry>
<Geometry x:Key="GitIcon">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</Geometry>
<Geometry x:Key="HtmlIcon">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</Geometry>
<Geometry x:Key="JavaScriptIcon">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</Geometry>
<Geometry x:Key="CssIcon">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</Geometry>
<Geometry x:Key="CsvIcon">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</Geometry>
<!-- For specific games-->
<Geometry x:Key="BorderlandsIcon">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</Geometry>
</ResourceDictionary>

View File

@ -43,6 +43,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@ -157,7 +158,8 @@
<TextBlock Grid.Row="9" Grid.Column="0" Text="Keep Directory Structure" VerticalAlignment="Center" Margin="0 5 0 5" ToolTip="Auto-save packages following their game directory" />
<CheckBox Grid.Row="9" Grid.Column="2" Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
IsChecked="{Binding KeepDirectoryStructure, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 0"/>
IsChecked="{Binding KeepDirectoryStructure, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 0"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" />
<Separator Grid.Row="10" Grid.Column="0" Grid.ColumnSpan="7" Style="{StaticResource CustomSeparator}" Tag="ADVANCED"></Separator>
@ -207,7 +209,8 @@
<CheckBox Grid.Row="14" Grid.Column="2" Margin="0 5 0 10"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.SettingsView}}}"
Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
IsChecked="{Binding SettingsView.MappingEndpoint.Overwrite, Mode=TwoWay}" />
IsChecked="{Binding SettingsView.MappingEndpoint.Overwrite, Mode=TwoWay}"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" />
<TextBlock Grid.Row="15" Grid.Column="0" Text="Mapping File Path" VerticalAlignment="Center" Margin="0 0 0 5"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.SettingsView}}}"
@ -221,29 +224,44 @@
<TextBlock Grid.Row="16" Grid.Column="0" Text="Serialize Script Bytecode" VerticalAlignment="Center" Margin="0 0 0 5" />
<CheckBox Grid.Row="16" Grid.Column="2" Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
IsChecked="{Binding ReadScriptData, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10"/>
IsChecked="{Binding ReadScriptData, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" />
<TextBlock Grid.Row="17" Grid.Column="0" Text="Serialize Inlined Shader Maps" VerticalAlignment="Center" Margin="0 0 0 5" />
<CheckBox Grid.Row="17" Grid.Column="2" Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
IsChecked="{Binding ReadShaderMaps, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10"/>
IsChecked="{Binding ReadShaderMaps, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" />
<TextBlock Grid.Row="18" Grid.Column="0" Text="Decompile Blueprint to Pseudo C++" VerticalAlignment="Center" Margin="0 0 0 5" ToolTip="Adds a right click option to decompile UClass packages into a pseudo C++ friendly format" />
<CheckBox Grid.Row="18" Grid.Column="2" Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
IsChecked="{Binding ShowDecompileOption, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10"/>
IsChecked="{Binding ShowDecompileOption, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" />
<TextBlock Grid.Row="19" Grid.Column="0" Text="Max Wwise Bank (.BNK) Prefetch" VerticalAlignment="Center" Margin="0 0 0 5" />
<Slider Grid.Row="19" Grid.Column="2" Grid.ColumnSpan="5" TickPlacement="None" Minimum="0" Maximum="2048" Ticks="0,8,32,128,256,512,1024,2048"
<TextBlock Grid.Row="19"
Grid.Column="0"
Text="Convert Audio During Export (.wav)"
VerticalAlignment="Center"
Margin="0 0 0 5" />
<CheckBox Grid.Row="19"
Grid.Column="2"
Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
IsChecked="{Binding ConvertAudioOnBulkExport, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}"
Margin="0 5 0 10"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" />
<TextBlock Grid.Row="20" Grid.Column="0" Text="Max Wwise Bank (.BNK) Prefetch" VerticalAlignment="Center" Margin="0 0 0 5" />
<Slider Grid.Row="20" Grid.Column="2" Grid.ColumnSpan="5" TickPlacement="None" Minimum="0" Maximum="2048" Ticks="0,8,32,128,256,512,1024,2048"
AutoToolTipPlacement="BottomRight" IsMoveToPointEnabled="True" IsSnapToTickEnabled="True" Margin="0 5 0 5"
Value="{Binding WwiseMaxBnkPrefetch, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}"/>
<TextBlock Grid.Row="20"
<TextBlock Grid.Row="21"
Grid.Column="0"
Text="CRIWARE Decryption Key"
VerticalAlignment="Center"
Margin="0 0 0 10" />
<TextBox x:Name="CriwareKeyBox"
Grid.Row="20"
Grid.Row="21"
Grid.Column="2"
Grid.ColumnSpan="5"
Margin="0 5 0 10"