Add unluac decompiler
Some checks failed
FModel QA Builder / build (push) Has been cancelled

Roco Kingdom: World lua support, Neverness to Everness ini decryption
This commit is contained in:
LongerWarrior 2026-05-01 21:59:00 +03:00
parent c279f1fd9c
commit 9159a91626
12 changed files with 306 additions and 11 deletions

@ -1 +1 @@
Subproject commit 3ea8a56be6e16012ebdeaab6e17f7b14049abda4
Subproject commit 81458ae77d3f3230d7582a77ffd938510430a4bf

View File

@ -164,3 +164,9 @@ public enum EAssetCategory : uint
RocoKingdomWorld = GameSpecific + 3,
DeltaForce = GameSpecific + 4,
}
public enum EUnluacMode
{
Decompile,
Disassemble,
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -44,6 +44,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@ -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}}" />
<TextBlock Grid.Row="19"
<TextBlock Grid.Row="19" Grid.Column="0" Text="Decompile Lua" VerticalAlignment="Center" Margin="0 0 0 5" ToolTip="Decompile/Disassemble compiled lua files with an unluac." />
<CheckBox Grid.Row="19" Grid.Column="2" Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
IsChecked="{Binding DecompileLua, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}"
Checked="OnDecompileLuaChanged" />
<TextBlock Grid.Row="20"
Grid.Column="0"
Text="Convert Audio During Export (.wav)"
VerticalAlignment="Center"
Margin="0 0 0 5" />
<CheckBox Grid.Row="19"
<CheckBox Grid.Row="20"
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"
<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"
@ -604,6 +611,98 @@
HotKey="{Binding NextAudio, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="unluacTemplate">
<Grid adonisExtensions:LayerExtension.Layer="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Mode" VerticalAlignment="Center" Margin="0 0 0 5" />
<ComboBox Grid.Row="0" Grid.Column="2" Grid.ColumnSpan="10"
SelectedValuePath="Tag"
SelectedValue="{Binding UnluacMode, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}"
Margin="0 0 0 5">
<ComboBoxItem Content="Decompile" Tag="{x:Static local:EUnluacMode.Decompile}" />
<ComboBoxItem Content="Disassemble" Tag="{x:Static local:EUnluacMode.Disassemble}" />
</ComboBox>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Raw string" VerticalAlignment="Center" Margin="0 0 0 5" />
<CheckBox Tag="RawString"
Grid.Row="1" Grid.Column="2" Grid.ColumnSpan="3"
Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" Margin="0 5 0 5"
IsChecked="{Binding UnluacFlags, Source={x:Static local:Settings.UserSettings.Default},
Converter={x:Static converters:EnumFlagToBoolConverter.Instance}, ConverterParameter=RawString, Mode=OneWay}"
Checked="OnUnluacFlagChanged"
Unchecked="OnUnluacFlagChanged"
ToolTip="Copy string bytes directly to output">
</CheckBox>
<TextBlock Grid.Row="1" Grid.Column="4" Text="No debug" VerticalAlignment="Center" Margin="0 0 0 5" />
<CheckBox Tag="NoDebug"
Grid.Row="1" Grid.Column="6" Grid.ColumnSpan="3"
Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" Margin="0 5 0 5"
IsChecked="{Binding UnluacFlags, Source={x:Static local:Settings.UserSettings.Default},
Converter={x:Static converters:EnumFlagToBoolConverter.Instance}, ConverterParameter=NoDebug, Mode=OneWay}"
Checked="OnUnluacFlagChanged"
Unchecked="OnUnluacFlagChanged"
ToolTip="Ignore debugging information in input file">
</CheckBox>
<TextBlock Grid.Row="1" Grid.Column="8" Text="Luaj" VerticalAlignment="Center" Margin="0 0 0 5" />
<CheckBox Tag="Luaj"
Grid.Row="1" Grid.Column="10" Grid.ColumnSpan="3"
Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" Margin="0 5 0 5"
IsChecked="{Binding UnluacFlags, Source={x:Static local:Settings.UserSettings.Default},
Converter={x:Static converters:EnumFlagToBoolConverter.Instance}, ConverterParameter=Luaj, Mode=OneWay}"
Checked="OnUnluacFlagChanged"
Unchecked="OnUnluacFlagChanged"
ToolTip="Emulate Luaj's permissive parser">
</CheckBox>
<Separator Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="11" Style="{StaticResource CustomSeparator}" />
<TextBlock Grid.Row="3"
Grid.Column="0"
Text="OpcodeMap"
VerticalAlignment="Top"
Margin="0 0 0 5"/>
<TextBox Grid.Row="3" Grid.Column="2" Grid.ColumnSpan="10"
Margin="0 0 0 5" Height="300"
FontSize="14" Padding="8"
HorizontalAlignment="Stretch" VerticalAlignment="Top"
VerticalContentAlignment="Top" TextAlignment="Left"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.SettingsView}}}"
Text="{Binding SettingsView.UnluacOpcodeMap, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
AcceptsReturn="True"
AcceptsTab="True"
TextWrapping="NoWrap"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
ToolTip="Paste a custom opcode map." />
</Grid>
</DataTemplate>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<Grid>
@ -681,6 +780,28 @@
</StackPanel>
</TreeViewItem.Header>
</TreeViewItem>
<TreeViewItem Tag="unluacTemplate">
<TreeViewItem.Header>
<StackPanel Orientation="Horizontal">
<Viewbox Width="16" Height="16" HorizontalAlignment="Center" Margin="-20 4 7.5 4">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource LuaIcon}" />
</Canvas>
</Viewbox>
<TextBlock Text="unluac" HorizontalAlignment="Left" VerticalAlignment="Center" />
</StackPanel>
</TreeViewItem.Header>
<TreeViewItem.Style>
<Style TargetType="TreeViewItem" BasedOn="{StaticResource TreeViewItemStyle}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding DecompileLua, Source={x:Static local:Settings.UserSettings.Default}}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</TreeViewItem.Style>
</TreeViewItem>
</TreeView>
<Grid Grid.Row="0" Grid.Column="1" Margin="{adonisUi:Space 1, 0.5}" HorizontalAlignment="Stretch">

View File

@ -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<EUnluacFlags>(name, true, out var flag)) return;
var current = UserSettings.Default.UnluacFlags;
var isChecked = cb.IsChecked == true;
UserSettings.Default.UnluacFlags = isChecked ? (current | flag) : (current & ~flag);
}
}