added links to some exceptions + auto detect ue version (#657)
Some checks failed
FModel QA Builder / build (push) Has been cancelled

Co-authored-by: Asval <asval.contactme@gmail.com>
Co-authored-by: Krowe-moh <27891447+Krowe-moh@users.noreply.github.com>
This commit is contained in:
Masusder 2026-03-13 21:14:56 +01:00 committed by GitHub
parent c9542f1a91
commit 639f21e574
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 243 additions and 74 deletions

@ -1 +1 @@
Subproject commit 7ed9bd5adf3daada4bd7d884f3a42d163a64247a
Subproject commit 6d7157a29b08d583aef9887a56d70f27a2ff36d5

View File

@ -40,6 +40,12 @@ public static class Constants
public const string _NO_PRESET_TRIGGER = "Hand Made";
// Common issues
public const string MAPPING_ISSUE_LINK = "https://github.com/4sval/FModel/discussions/418";
public const string AUDIO_ISSUE_LINK = "https://github.com/4sval/FModel/discussions/658";
public const string RADA_ISSUE_LINK = "https://github.com/4sval/FModel/discussions/422";
public const string VERSION_ISSUE_LINK = "https://github.com/4sval/FModel/discussions/425";
public static int PALETTE_LENGTH => COLOR_PALETTE.Length;
public static readonly Vector3[] COLOR_PALETTE =
{

View File

@ -252,13 +252,6 @@ namespace FModel.Settings
set => SetProperty(ref _imageMergerMargin, value);
}
private bool _canExportRawData;
public bool CanExportRawData
{
get => _canExportRawData;
set => SetProperty(ref _canExportRawData, value);
}
private bool _readScriptData;
public bool ReadScriptData
{

View File

@ -246,7 +246,7 @@ public class ApplicationViewModel : ViewModel
}
else
{
FLogger.Append(ELog.Error, () => FLogger.Text("Could not download VgmStream", Constants.WHITE, true));
FLogger.Append(ELog.Error, () => FLogger.Text("Could not download vgmstream", Constants.WHITE, true));
}
}
}

View File

@ -298,13 +298,23 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
Save(a, true);
}
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved audio from ", Constants.WHITE);
FLogger.Link(_audioFiles.First().FileName, _audioFiles.First().FilePath, true);
});
if (_audioFiles.Count > 1)
FLogger.Append(ELog.Information, () => FLogger.Text($"Successfully saved {_audioFiles.Count} audio files", Constants.WHITE, true));
{
var dir = new DirectoryInfo(Path.GetDirectoryName(_audioFiles.First().FilePath));
FLogger.Append(ELog.Information, () =>
{
FLogger.Text($"Successfully saved {_audioFiles.Count} audio files to ", Constants.WHITE);
FLogger.Link(dir.Name, dir.FullName, true);
});
}
else
{
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved ", Constants.WHITE);
FLogger.Link(_audioFiles.First().FileName, _audioFiles.First().FilePath, true);
});
}
});
}
@ -654,15 +664,24 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
}
}
private bool TryConvert(out string wavFilePath) => TryConvert(SelectedAudioFile.FilePath, SelectedAudioFile.Data, out wavFilePath);
public static bool TryConvert(string inputFilePath, byte[] inputFileData, out string wavFilePath)
private bool TryConvert(out string wavFilePath) => TryConvert(SelectedAudioFile.FilePath, SelectedAudioFile.Data, out wavFilePath, true);
public static bool TryConvert(string inputFilePath, byte[] inputFileData, out string wavFilePath, bool updateUi = false)
{
wavFilePath = string.Empty;
var vgmFilePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "test.exe");
if (!File.Exists(vgmFilePath))
{
vgmFilePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "vgmstream-cli.exe");
if (!File.Exists(vgmFilePath)) return false;
if (!File.Exists(vgmFilePath))
{
Log.Error("Failed to convert {InputFilePath}, vgmstream is missing", inputFilePath);
FLogger.Append(ELog.Error, () =>
{
FLogger.Text("Failed to convert audio because vgmstream is missing. See: ", Constants.WHITE);
FLogger.Link("→ link ←", Constants.AUDIO_ISSUE_LINK, true);
});
return false;
}
}
Directory.CreateDirectory(inputFilePath.SubstringBeforeLast("/"));
@ -679,7 +698,22 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
vgmProcess?.WaitForExit(5000);
File.Delete(inputFilePath);
return vgmProcess?.ExitCode == 0 && File.Exists(wavFilePath);
var success = vgmProcess?.ExitCode == 0 && File.Exists(wavFilePath);
if (!success)
{
Log.Error("Failed to convert {InputFilePath} to .wav format", inputFilePath);
if (updateUi)
{
FLogger.Append(ELog.Error, () =>
{
FLogger.Text("Failed to convert audio to .wav format. See: ", Constants.WHITE);
FLogger.Link("→ link ←", Constants.AUDIO_ISSUE_LINK, true);
});
}
}
return success;
}
private bool TryDecode(string extension, out string rawFilePath)
@ -688,6 +722,12 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
var decoderPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", $"{extension}dec.exe");
if (!File.Exists(decoderPath))
{
Log.Error("Failed to convert {FilePath}, rada decoder is missing", SelectedAudioFile.FilePath);
FLogger.Append(ELog.Error, () =>
{
FLogger.Text("Failed to convert audio because rada decoder is missing. See: ", Constants.WHITE);
FLogger.Link("→ link ←", Constants.RADA_ISSUE_LINK, true);
});
return false;
}

View File

@ -1179,7 +1179,7 @@ public class CUE4ParseViewModel : ViewModel
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)
if (pointer.Object.Value is UAkMediaAssetData dataObj && dataObj.Outer.Object.Value is UAkMediaAsset)
return false;
var shouldDecompress = UserSettings.Default.CompressedAudioMode == ECompressedAudio.PlayDecompressed;
@ -1196,7 +1196,7 @@ public class CUE4ParseViewModel : ViewModel
}
case UAkMediaAsset when (isNone || saveAudio) && pointer.Object.Value is UAkMediaAsset akMediaAsset:
{
var audioName = akMediaAsset.MediaName;
var audioName = akMediaAsset.MediaName ?? akMediaAsset.Name;
if (akMediaAsset.CurrentMediaAssetData?.TryLoad<UAkMediaAssetData>(out var akMediaAssetData) is true)
{
var shouldDecompress = UserSettings.Default.CompressedAudioMode is ECompressedAudio.PlayDecompressed;
@ -1416,25 +1416,15 @@ public class CUE4ParseViewModel : ViewModel
writer.Flush();
}
bool conversionSuccess = true;
if (UserSettings.Default.ConvertAudioOnBulkExport)
{
AudioPlayerViewModel.TryConvert(savedAudioPath, data, out string wavFilePath);
if (!string.IsNullOrEmpty(wavFilePath))
{
savedAudioPath = wavFilePath;
}
else if (updateUi)
{
FLogger.Append(ELog.Error, () =>
{
FLogger.Text("Failed to convert audio to WAV format, aborting extraction.", Constants.WHITE, true);
});
return;
}
conversionSuccess = AudioPlayerViewModel.TryConvert(savedAudioPath, data, out string wavFilePath);
if (conversionSuccess) savedAudioPath = wavFilePath;
}
Log.Information("Successfully saved {FilePath}", savedAudioPath);
if (updateUi)
if (updateUi && conversionSuccess)
{
FLogger.Append(ELog.Information, () =>
{

View File

@ -356,6 +356,7 @@ public class GameFileViewModel(GameFile asset) : ViewModel
case "csv":
AssetCategory = EAssetCategory.Data;
break;
case "stinfo":
case "ushaderbytecode":
AssetCategory = EAssetCategory.ByteCode;
break;

View File

@ -4,6 +4,8 @@ using Serilog;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
@ -67,12 +69,103 @@ public class GameSelectorViewModel : ViewModel
public void AddUndetectedDir(string gameDirectory) => AddUndetectedDir(gameDirectory.SubstringAfterLast('\\'), gameDirectory);
public void AddUndetectedDir(string gameName, string gameDirectory)
{
var setting = DirectorySettings.Default(gameName, gameDirectory, true);
if (TryDetectUeVersion(gameDirectory, out var ueVersion, out var newGameDirectory))
{
// gameDirectory = newGameDirectory; // directory was changed to point to the correct paks folder
}
var setting = DirectorySettings.Default(gameName, gameDirectory, true, ueVersion);
UserSettings.Default.PerDirectory[gameDirectory] = setting;
_detectedDirectories.Add(setting);
SelectedDirectory = DetectedDirectories.Last();
}
private bool TryDetectUeVersion(string gameDirectory, out EGame ueVersion, [MaybeNullWhen(false)] out string newGameDirectory)
{
var targetGameDir = gameDirectory;
if (!targetGameDir.EndsWith("Paks", StringComparison.OrdinalIgnoreCase))
{
var dirs = Directory.GetDirectories(targetGameDir, "Paks", SearchOption.AllDirectories);
var paksDir = dirs.Length == 1 ? dirs[0] : dirs.FirstOrDefault(x => !x.EndsWith("Engine\\Programs\\CrashReportClient\\Content\\Paks"));
if (!string.IsNullOrEmpty(paksDir))
{
Log.Warning("Selected directory \"{GameDirectory}\" does not end with \"Paks\". Looking in \"{PaksDir}\" instead.", targetGameDir, paksDir);
targetGameDir = paksDir;
}
if (Directory.GetFiles(gameDirectory, "*.exe") is { Length: 1 } exe && TryGetUeVersionFromExe(exe[0], out ueVersion))
{
// we checked the exe in the original directory, the BootstrapPackagedGame one
// but we still want c4p to use the paks folder as the game directory (if any), not the original one
newGameDirectory = targetGameDir;
Log.Information("Detected UE version {UeVersion} from \"{Exe}\"", ueVersion, exe[0]);
return true;
}
}
// past this point, we assume targetGameDir is the correct Paks folder
newGameDirectory = targetGameDir;
var projectDir = Path.Combine(targetGameDir, "..", "..");
var projectBinariesDir = Path.Combine(projectDir, "Binaries", "Win64");
if (Directory.Exists(projectBinariesDir))
{
if (Directory.GetFiles(projectBinariesDir, "*-Win64-Shipping.exe") is { Length: > 0 } shipping)
{
foreach (var exe in shipping)
{
if (TryGetUeVersionFromExe(exe, out ueVersion))
{
Log.Information("Detected UE version {UeVersion} from \"{Exe}\"", ueVersion, exe);
return true;
}
}
}
else if (Directory.GetFiles(projectBinariesDir, "*.exe") is { Length: < 3 } exes)
{
foreach (var exe in exes)
{
if (TryGetUeVersionFromExe(exe, out ueVersion))
{
Log.Information("Detected UE version {UeVersion} from \"{Exe}\"", ueVersion, exe);
return true;
}
}
}
}
var crashReportClientExe = Path.Combine(projectDir, "..", "Engine", "Binaries", "Win64", "CrashReportClient.exe");
if (File.Exists(crashReportClientExe) && TryGetUeVersionFromExe(crashReportClientExe, out ueVersion))
{
Log.Information("Detected UE version {UeVersion} from \"{Exe}\"", ueVersion, crashReportClientExe);
return true;
}
ueVersion = EGame.GAME_UE4_LATEST;
Log.Warning("Failed to detect UE version for \"{GameDirectory}\".", gameDirectory);
return false;
}
private bool TryGetUeVersionFromExe(string exePath, out EGame ueVersion)
{
ueVersion = EGame.GAME_UE4_LATEST;
try
{
var info = FileVersionInfo.GetVersionInfo(exePath);
ueVersion = info.FileMajorPart switch
{
4 => (EGame) Math.Min((uint)(GameUtils.GameUe4Base + (info.FileMinorPart << 16)), (uint) EGame.GAME_UE4_LATEST),
5 => (EGame) Math.Min((uint)(GameUtils.GameUe5Base + (info.FileMinorPart << 16)), (uint) EGame.GAME_UE5_LATEST),
_ => throw new InvalidOperationException($"Unsupported UE major version {info.FileMajorPart} detected from {exePath}")
};
return true;
}
catch
{
return false;
}
}
public void DeleteSelectedGame()
{
UserSettings.Default.PerDirectory.Remove(SelectedDirectory.GameDirectory); // should not be a problem

View File

@ -1,6 +1,7 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using CUE4Parse.UE4.Exceptions;
using FModel.Framework;
using FModel.Services;
using FModel.Views.Resources.Controls;
@ -100,7 +101,26 @@ public class ThreadWorkerViewModel : ViewModel
CurrentCancellationTokenSource = null; // kill token
Log.Error("{Exception}", e);
FLogger.Append(e);
switch (e)
{
case MappingException:
FLogger.Append(ELog.Error, () =>
{
FLogger.Text("Package has unversioned properties but mapping file (.usmap) is missing, can't serialize. See: ", Constants.WHITE);
FLogger.Link("→ link ←", Constants.MAPPING_ISSUE_LINK, true);
});
break;
case VersionException v: // Error might be unrelated to version, but it's usually the case
FLogger.Append(ELog.Error, () =>
{
FLogger.Text(v.Message[..^1] + ", can't serialize. Make sure the correct UE version is configured. See: ", Constants.WHITE);
FLogger.Link("→ link ←", Constants.VERSION_ISSUE_LINK, true);
});
break;
default:
FLogger.Append(e);
break;
}
return;
}
}

View File

@ -110,8 +110,7 @@
</MenuItem.Style>
</MenuItem>
<Separator />
<MenuItem Command="{Binding RightClickMenuCommand}"
Visibility="{Binding CanExportRawData, Source={x:Static settings:UserSettings.Default}, Converter={StaticResource BoolToVisibilityConverter}}">
<MenuItem Command="{Binding RightClickMenuCommand}">
<MenuItem.Header>
<TextBlock
Text="{Binding PlacementTarget.SelectedItem.Asset.Extension,

View File

@ -1,14 +1,12 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:settings="clr-namespace:FModel.Settings"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
x:Class="FModel.Views.Resources.Controls.ContextMenus.FolderContextMenuDictionary">
<ContextMenu x:Key="FolderContextMenu" x:Shared="False"
Opened="FolderContextMenu_OnOpened">
<MenuItem Header="Export Folder's Packages Raw Data (.uasset)"
Command="{Binding RightClickMenuCommand}"
Visibility="{Binding CanExportRawData, Source={x:Static settings:UserSettings.Default}, Converter={StaticResource BoolToVisibilityConverter}}">
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Folders_Export_Data" />

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
@ -126,13 +126,40 @@ public class FLogger : ITextFormatter
{
NavigateUri = new Uri(url),
OverridesDefaultStyle = true,
Style = new Style(typeof(Hyperlink)) { Setters =
Style = new Style(typeof(Hyperlink))
{
new Setter(FrameworkContentElement.CursorProperty, Cursors.Hand),
new Setter(TextBlock.TextDecorationsProperty, TextDecorations.Underline),
new Setter(TextElement.ForegroundProperty, Brushes.Cornsilk)
}}
}.Click += (sender, _) => Process.Start("explorer.exe", $"/select, \"{((Hyperlink)sender).NavigateUri.AbsoluteUri}\"");
Setters =
{
new Setter(FrameworkContentElement.CursorProperty, Cursors.Hand),
new Setter(TextElement.ForegroundProperty, Brushes.Goldenrod),
new Setter(TextElement.FontWeightProperty, FontWeights.Bold)
},
Triggers =
{
new Trigger
{
Property = UIElement.IsMouseOverProperty,
Value = true,
Setters =
{
new Setter(TextElement.ForegroundProperty, Brushes.Gold),
new Setter(TextBlock.TextDecorationsProperty, TextDecorations.Underline)
}
}
}
}
}.Click += (sender, _) =>
{
var uri = ((Hyperlink) sender).NavigateUri;
if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)
{
Process.Start(new ProcessStartInfo(uri.AbsoluteUri) { UseShellExecute = true });
}
else
{
Process.Start("explorer.exe", $"/select, \"{uri.AbsoluteUri}\"");
}
};
}
finally
{

View File

@ -926,8 +926,7 @@
</MenuItem.IsEnabled>
</MenuItem>
<Separator />
<MenuItem Command="{Binding TabCommand}" CommandParameter="Asset_Export_Data"
Visibility="{Binding CanExportRawData, Source={x:Static settings:UserSettings.Default}, Converter={StaticResource BoolToVisibilityConverter}}">
<MenuItem Command="{Binding TabCommand}" CommandParameter="Asset_Export_Data">
<MenuItem.Header>
<TextBlock Text="{Binding Entry.Extension, FallbackValue='Export Raw Data', StringFormat='Export Raw Data (.{0})'}" />
</MenuItem.Header>

View File

@ -1,7 +1,6 @@
<adonisControls:AdonisWindow x:Class="FModel.Views.SearchView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:settings="clr-namespace:FModel.Settings"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI"
@ -210,8 +209,7 @@
</MenuItem.IsEnabled>
</MenuItem>
<Separator />
<MenuItem Command="{Binding DataContext.mainApplication.RightClickMenuCommand}"
Visibility="{Binding CanExportRawData, Source={x:Static settings:UserSettings.Default}, Converter={StaticResource BoolToVisibilityConverter}}">
<MenuItem Command="{Binding DataContext.mainApplication.RightClickMenuCommand}">
<MenuItem.Header>
<TextBlock Text="{Binding DataContext.SelectedItem.Extension, FallbackValue='Export Raw Data',
StringFormat='Export Raw Data (.{0})', RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
@ -556,8 +554,7 @@
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Command="{Binding DataContext.mainApplication.RightClickMenuCommand}"
Visibility="{Binding CanExportRawData, Source={x:Static settings:UserSettings.Default}, Converter={StaticResource BoolToVisibilityConverter}}">
<MenuItem Command="{Binding DataContext.mainApplication.RightClickMenuCommand}">
<MenuItem.Header>
<TextBlock Text="{Binding DataContext.SelectedItem.Extension, FallbackValue='Export Raw Data',
StringFormat='Export Raw Data (.{0})', RelativeSource={RelativeSource AncestorType=ContextMenu}}" />

View File

@ -44,7 +44,6 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@ -223,51 +222,46 @@
<Button Grid.Column="2" Content="Mapping" Click="OpenMappingEndpoint" />
</Grid>
<TextBlock Grid.Row="16" Grid.Column="0" Text="Allow Raw Data Export (.uasset)" VerticalAlignment="Center" Margin="0 0 0 5" />
<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 CanExportRawData, 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 Script Bytecode" 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 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="18" Grid.Column="0" Text="Serialize Inlined Shader Maps" VerticalAlignment="Center" Margin="0 0 0 5" />
<CheckBox Grid.Row="18" Grid.Column="2" Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
<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"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" />
<TextBlock Grid.Row="19" 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="19" Grid.Column="2" Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
<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"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" />
<TextBlock Grid.Row="20"
<TextBlock Grid.Row="19"
Grid.Column="0"
Text="Convert Audio During Export (.wav)"
VerticalAlignment="Center"
Margin="0 0 0 5" />
<CheckBox Grid.Row="20"
<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="21" Grid.Column="0" Text="Max Wwise Bank (.BNK) Prefetch" VerticalAlignment="Center" Margin="0 0 0 5" />
<Slider Grid.Row="21" Grid.Column="2" Grid.ColumnSpan="5" TickPlacement="None" Minimum="0" Maximum="2048" Ticks="0,8,32,128,256,512,1024,2048"
<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="22"
<TextBlock Grid.Row="21"
Grid.Column="0"
Text="CRIWARE Decryption Key"
VerticalAlignment="Center"
Margin="0 0 0 10" />
<TextBox x:Name="CriwareKeyBox"
Grid.Row="22"
Grid.Row="21"
Grid.Column="2"
Grid.ColumnSpan="5"
Margin="0 5 0 10"
@ -398,6 +392,8 @@
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Setter Property="Visibility" Value="Collapsed" />
<EventSetter Event="Hyperlink.Click" Handler="OnHyperlinkClick" />
<Style.Triggers>
<DataTrigger Binding="{Binding DataContext.SettingsView.SelectedMeshExportFormat, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.SettingsView}}}" Value="{x:Static c4pMeshes:EMeshFormat.UEFormat}">
<Setter Property="ContentTemplate">

View File

@ -1,9 +1,11 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using FModel.Services;
using FModel.Settings;
using FModel.ViewModels;
@ -241,4 +243,12 @@ public partial class SettingsView
out value
);
}
private void OnHyperlinkClick(object sender, RoutedEventArgs e)
{
if (e.OriginalSource is not Hyperlink hyperlink)
return;
Process.Start(new ProcessStartInfo(hyperlink.NavigateUri.AbsoluteUri) { UseShellExecute = true });
}
}

View File

@ -93,7 +93,7 @@ public abstract class UModel : IRenderableModel
{
_export = export;
Path = _export.GetPathName();
Name = Path.SubstringAfterLast('/').SubstringBefore('.');
Name = export.Name;
Type = export.ExportType;
UvCount = 1;