From 9159a91626b49b50a8e95bc22cba05aa19431c29 Mon Sep 17 00:00:00 2001 From: LongerWarrior Date: Fri, 1 May 2026 21:59:00 +0300 Subject: [PATCH] Add unluac decompiler Roco Kingdom: World lua support, Neverness to Everness ini decryption --- CUE4Parse | 2 +- FModel/Enums.cs | 6 + FModel/MainWindow.xaml.cs | 3 +- FModel/Settings/DirectorySettings.cs | 10 +- FModel/Settings/UserSettings.cs | 31 +++++ FModel/ViewModels/ApplicationViewModel.cs | 9 ++ FModel/ViewModels/AudioPlayerViewModel.cs | 2 +- FModel/ViewModels/CUE4ParseViewModel.cs | 65 ++++++++- FModel/ViewModels/SettingsViewModel.cs | 10 ++ .../Converters/EnumFlagToBoolConverter.cs | 32 +++++ FModel/Views/SettingsView.xaml | 129 +++++++++++++++++- FModel/Views/SettingsView.xaml.cs | 18 +++ 12 files changed, 306 insertions(+), 11 deletions(-) create mode 100644 FModel/Views/Resources/Converters/EnumFlagToBoolConverter.cs diff --git a/CUE4Parse b/CUE4Parse index 3ea8a56b..81458ae7 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 3ea8a56be6e16012ebdeaab6e17f7b14049abda4 +Subproject commit 81458ae77d3f3230d7582a77ffd938510430a4bf diff --git a/FModel/Enums.cs b/FModel/Enums.cs index 59c16b75..04f34036 100644 --- a/FModel/Enums.cs +++ b/FModel/Enums.cs @@ -164,3 +164,9 @@ public enum EAssetCategory : uint RocoKingdomWorld = GameSpecific + 3, DeltaForce = GameSpecific + 4, } + +public enum EUnluacMode +{ + Decompile, + Disassemble, +} diff --git a/FModel/MainWindow.xaml.cs b/FModel/MainWindow.xaml.cs index b36da622..fbed15d4 100644 --- a/FModel/MainWindow.xaml.cs +++ b/FModel/MainWindow.xaml.cs @@ -124,7 +124,8 @@ public partial class MainWindow { if (UserSettings.Default.DiscordRpc == EDiscordRpc.Always) _discordHandler.Initialize(_applicationView.GameDisplayName); - }) + }), + UserSettings.Default.DecompileLua ? ApplicationViewModel.InitUnluac() : Task.CompletedTask ).ConfigureAwait(false); #if DEBUG diff --git a/FModel/Settings/DirectorySettings.cs b/FModel/Settings/DirectorySettings.cs index 8cf126c7..45de629b 100644 --- a/FModel/Settings/DirectorySettings.cs +++ b/FModel/Settings/DirectorySettings.cs @@ -25,7 +25,8 @@ public class DirectorySettings : ViewModel, ICloneable Directories = old?.Directories ?? CustomDirectory.Default(gameName), AesKeys = old?.AesKeys ?? new AesResponse { MainKey = aes, DynamicKeys = null }, LastAesReload = old?.LastAesReload ?? DateTime.Today.AddDays(-1), - CriwareDecryptionKey = old?.CriwareDecryptionKey ?? 0 + CriwareDecryptionKey = old?.CriwareDecryptionKey ?? 0, + UnluacOpCodeMap = old?.UnluacOpCodeMap ?? "" }; } @@ -106,6 +107,13 @@ public class DirectorySettings : ViewModel, ICloneable set => SetProperty(ref _criwareDecryptionKey, value); } + private string _unluacOpCodeMap; + public string UnluacOpCodeMap + { + get => _unluacOpCodeMap; + set => SetProperty(ref _unluacOpCodeMap, value); + } + private bool Equals(DirectorySettings other) { return GameDirectory == other.GameDirectory && UeVersion == other.UeVersion; diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs index bca1427d..ea62298c 100644 --- a/FModel/Settings/UserSettings.cs +++ b/FModel/Settings/UserSettings.cs @@ -11,6 +11,7 @@ using CUE4Parse_Conversion.Animations; using CUE4Parse_Conversion.Meshes; using CUE4Parse_Conversion.Textures; using CUE4Parse_Conversion.UEFormat.Enums; +using CUE4Parse.UE4.Lua.unluac; using FModel.Framework; using FModel.ViewModels; using FModel.ViewModels.ApiEndpoints.Models; @@ -280,6 +281,36 @@ namespace FModel.Settings set => SetProperty(ref _convertAudioOnBulkExport, value); } + private bool _decompileLua; + public bool DecompileLua + { + get => _decompileLua; + set => SetProperty(ref _decompileLua, value); + } + + [JsonIgnore] + public EUnluacMode UnluacMode + { + get => UnluacFlags.HasFlag(EUnluacFlags.Disassemble) ? EUnluacMode.Disassemble : EUnluacMode.Decompile; + set + { + var withoutMode = UnluacFlags & ~(EUnluacFlags.Decompile | EUnluacFlags.Disassemble); + var modeFlag = value == EUnluacMode.Disassemble ? EUnluacFlags.Disassemble : EUnluacFlags.Decompile; + UnluacFlags = withoutMode | modeFlag; + } + } + + private EUnluacFlags _unluacFlags; + public EUnluacFlags UnluacFlags + { + get => _unluacFlags; + set + { + if (!SetProperty(ref _unluacFlags, value)) return; + RaisePropertyChanged(nameof(UnluacMode)); + } + } + private IDictionary _perDirectory = new Dictionary(); public IDictionary PerDirectory { diff --git a/FModel/ViewModels/ApplicationViewModel.cs b/FModel/ViewModels/ApplicationViewModel.cs index 42b24d90..c16619b0 100644 --- a/FModel/ViewModels/ApplicationViewModel.cs +++ b/FModel/ViewModels/ApplicationViewModel.cs @@ -9,6 +9,7 @@ using System.Windows; using CUE4Parse_Conversion.Textures.BC; using CUE4Parse.Compression; using CUE4Parse.Encryption.Aes; +using CUE4Parse.UE4.Lua.unluac; using CUE4Parse.UE4.Objects.Core.Misc; using CUE4Parse.UE4.VirtualFileSystem; using FModel.Extensions; @@ -340,4 +341,12 @@ public class ApplicationViewModel : ViewModel DetexHelper.Initialize(detexPath); } + + public static async Task InitUnluac() + { + var unluacPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", UnluacHelper.DllName); + await UnluacHelper.InitializeAsync(unluacPath).ConfigureAwait(false); + if (UnluacHelper.Instance is null) + FLogger.Append(ELog.Error, () => FLogger.Text("Failed to download unluac", Constants.WHITE, true)); + } } diff --git a/FModel/ViewModels/AudioPlayerViewModel.cs b/FModel/ViewModels/AudioPlayerViewModel.cs index 9821e915..81a52065 100644 --- a/FModel/ViewModels/AudioPlayerViewModel.cs +++ b/FModel/ViewModels/AudioPlayerViewModel.cs @@ -512,7 +512,7 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable if (Spectrum != null && PlayedFile.PlaybackState == PlaybackState.Playing) { - FftData = new float[4096]; + FftData = new float[4096+4]; Spectrum.GetFftData(FftData); RaiseSourcePropertyChangedEvent(ESourceProperty.FftData, FftData); } diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index defdd55c..a4363437 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -43,12 +43,14 @@ using CUE4Parse.UE4.Assets.Exports.StaticMesh; using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse.UE4.Assets.Exports.Verse; using CUE4Parse.UE4.Assets.Exports.Wwise; +using CUE4Parse.UE4.Assets.Objects; using CUE4Parse.UE4.BinaryConfig; using CUE4Parse.UE4.CriWare; using CUE4Parse.UE4.CriWare.Readers; using CUE4Parse.UE4.FMod; using CUE4Parse.UE4.IO; using CUE4Parse.UE4.Localization; +using CUE4Parse.UE4.Lua.unluac; using CUE4Parse.UE4.Objects.Core.Serialization; using CUE4Parse.UE4.Objects.Engine; using CUE4Parse.UE4.Objects.UObject; @@ -698,6 +700,18 @@ public class CUE4ParseViewModel : ViewModel ProcessCacheDBFile(entry, updateUi, saveProperties); break; } + case "luac": + case "lua": + { + var data = Provider.SaveAsset(entry); + byte[] decompiled = ProcessLuaFile(data); + + using var stream = new MemoryStream(decompiled); + using var reader = new StreamReader(stream); + TabControl.SelectedTab.SetDocumentText(reader.ReadToEnd(), saveProperties, updateUi); + + break; + } case "upluginmanifest": case "code-workspace": case "projectstore": @@ -749,7 +763,6 @@ public class CUE4ParseViewModel : ViewModel case "apx": case "udn": case "doc": - case "lua": case "vdf": case "yml": case "js": @@ -843,7 +856,7 @@ public class CUE4ParseViewModel : ViewModel case "pck": { var archive = entry.CreateReader(); - var wwise = new WwiseReader(archive, new WwiseGameFileSource(entry)); + var wwise = new WwiseReader(new FWwiseArchive(archive), new WwiseGameFileSource(entry)); TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(wwise, Formatting.Indented), saveProperties, updateUi); var medias = WwiseProvider.ExtractBankSounds(wwise); @@ -982,7 +995,6 @@ public class CUE4ParseViewModel : ViewModel break; } case "res": // just skip - case "luac": // compiled lua case "bytes": // wuthering waves break; default: @@ -1088,6 +1100,53 @@ public class CUE4ParseViewModel : ViewModel } } + private byte[] ProcessLuaFile(byte[] data) + { + var result = EUnluacErrorCode.Ok; + byte[] output = []; + if (BitConverter.ToUInt32(data) == UnluacHelper.LuaMagic && UnluacHelper.Instance is not null) + { + // opcodemap patch + byte[] opmapData = Provider.Versions.Game switch + { + _ => [], + }; + + var flags = UserSettings.Default.UnluacFlags; + var opcodemap = UserSettings.Default.CurrentDir.UnluacOpCodeMap; + if (!string.IsNullOrWhiteSpace(opcodemap)) + { + opmapData = Encoding.UTF8.GetBytes(opcodemap); + flags |= EUnluacFlags.OpCodeMap; + } + else if (opmapData is { Length: > 12 }) + { + flags |= EUnluacFlags.OpCodeMapPatch; + } + + result = UnluacHelper.Decompile(data, opmapData, (uint)flags, out output, out var log); + if (result != EUnluacErrorCode.Ok && log.Length > 0) + { + Log.Error(Encoding.UTF8.GetString(log)); + } + } + else + { + result = EUnluacErrorCode.Error; + } + + var decompiled = result switch + { + EUnluacErrorCode.Ok => output, +#if DEBUG + EUnluacErrorCode.PartialDecompile => output, +#endif + _ => data, + }; + + return decompiled; + } + public void ExtractAndScroll(CancellationToken cancellationToken, string fullPath, string objectName, string parentExportType) { Log.Information("User CTRL-CLICKED to extract '{FullPath}'", fullPath); diff --git a/FModel/ViewModels/SettingsViewModel.cs b/FModel/ViewModels/SettingsViewModel.cs index 626f4227..0d6ace36 100644 --- a/FModel/ViewModels/SettingsViewModel.cs +++ b/FModel/ViewModels/SettingsViewModel.cs @@ -172,6 +172,13 @@ public class SettingsViewModel : ViewModel set => SetProperty(ref _criwareDecryptionKey, value); } + private string _unluacOpcodeMap; + public string UnluacOpcodeMap + { + get => _unluacOpcodeMap; + set => SetProperty(ref _unluacOpcodeMap, value); + } + public bool SocketSettingsEnabled => SelectedMeshExportFormat == EMeshFormat.ActorX; public bool CompressionSettingsEnabled => SelectedMeshExportFormat == EMeshFormat.UEFormat; @@ -237,6 +244,7 @@ public class SettingsViewModel : ViewModel _optionsSnapshot = UserSettings.Default.CurrentDir.Versioning.Options; _mapStructTypesSnapshot = UserSettings.Default.CurrentDir.Versioning.MapStructTypes; _criwareDecryptionKey = UserSettings.Default.CurrentDir.CriwareDecryptionKey; + _unluacOpcodeMap = UserSettings.Default.CurrentDir.UnluacOpCodeMap; AesEndpoint = UserSettings.Default.CurrentDir.Endpoints[0]; MappingEndpoint = UserSettings.Default.CurrentDir.Endpoints[1]; @@ -273,6 +281,7 @@ public class SettingsViewModel : ViewModel SelectedMaterialExportFormat = _materialExportFormatSnapshot; SelectedTextureExportFormat = _textureExportFormatSnapshot; CriwareDecryptionKey = _criwareDecryptionKey; + UnluacOpcodeMap = _unluacOpcodeMap; SelectedAesReload = UserSettings.Default.AesReload; SelectedDiscordRpc = UserSettings.Default.DiscordRpc; @@ -314,6 +323,7 @@ public class SettingsViewModel : ViewModel UserSettings.Default.CurrentDir.Versioning.Options = SelectedOptions; UserSettings.Default.CurrentDir.Versioning.MapStructTypes = SelectedMapStructTypes; UserSettings.Default.CurrentDir.CriwareDecryptionKey = CriwareDecryptionKey; + UserSettings.Default.CurrentDir.UnluacOpCodeMap = UnluacOpcodeMap; UserSettings.Default.AssetLanguage = SelectedAssetLanguage; UserSettings.Default.CompressedAudioMode = SelectedCompressedAudio; diff --git a/FModel/Views/Resources/Converters/EnumFlagToBoolConverter.cs b/FModel/Views/Resources/Converters/EnumFlagToBoolConverter.cs new file mode 100644 index 00000000..5dffe662 --- /dev/null +++ b/FModel/Views/Resources/Converters/EnumFlagToBoolConverter.cs @@ -0,0 +1,32 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace FModel.Views.Resources.Converters; + +public sealed class EnumFlagToBoolConverter : IValueConverter +{ + public static readonly EnumFlagToBoolConverter Instance = new(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is null || parameter is null) return false; + + var enumType = value.GetType(); + if (!enumType.IsEnum) return false; + + var flag = parameter is string s + ? Enum.Parse(enumType, s, ignoreCase: true) + : parameter; + + var current = System.Convert.ToInt64(value); + var wanted = System.Convert.ToInt64(flag); + + return (current & wanted) == wanted; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/FModel/Views/SettingsView.xaml b/FModel/Views/SettingsView.xaml index b38b62d5..c7e8a1bf 100644 --- a/FModel/Views/SettingsView.xaml +++ b/FModel/Views/SettingsView.xaml @@ -44,6 +44,7 @@ + @@ -237,26 +238,32 @@ IsChecked="{Binding ShowDecompileOption, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" /> - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -681,6 +780,28 @@ + + + + + + + + + + + + + + + diff --git a/FModel/Views/SettingsView.xaml.cs b/FModel/Views/SettingsView.xaml.cs index ba73d7f8..74e428ef 100644 --- a/FModel/Views/SettingsView.xaml.cs +++ b/FModel/Views/SettingsView.xaml.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; +using CUE4Parse.UE4.Lua.unluac; using FModel.Services; using FModel.Settings; using FModel.ViewModels; @@ -274,4 +275,21 @@ public partial class SettingsView Process.Start(new ProcessStartInfo(hyperlink.NavigateUri.AbsoluteUri) { UseShellExecute = true }); } + + private async void OnDecompileLuaChanged(object sender, RoutedEventArgs e) + { + if (sender is CheckBox { IsChecked: true } && UnluacHelper.Instance is null) + await ApplicationViewModel.InitUnluac(); + } + + private void OnUnluacFlagChanged(object sender, RoutedEventArgs e) + { + if (sender is not CheckBox cb || cb.Tag is not string name) return; + if (!Enum.TryParse(name, true, out var flag)) return; + + var current = UserSettings.Default.UnluacFlags; + var isChecked = cb.IsChecked == true; + + UserSettings.Default.UnluacFlags = isChecked ? (current | flag) : (current & ~flag); + } }