FModel/FModel/Utils/Assets.cs
2020-11-21 21:54:30 +01:00

548 lines
29 KiB
C#

using System;
using System.Linq;
using System.Collections.Generic;
using FModel.ViewModels.ImageBox;
using Newtonsoft.Json;
using FModel.Logger;
using System.IO;
using FModel.ViewModels.AvalonEdit;
using System.Threading.Tasks;
using FModel.ViewModels.StatusBar;
using System.Collections;
using FModel.ViewModels.ListBox;
using System.Diagnostics;
using FModel.Windows.SoundPlayer;
using System.Windows;
using FModel.Windows.CustomNotifier;
using FModel.ViewModels.Buttons;
using System.Threading;
using SkiaSharp;
using System.Text;
using FModel.ViewModels.DataGrid;
using ICSharpCode.AvalonEdit.Highlighting;
using static FModel.Creator.Creator;
using System.Runtime.CompilerServices;
using FModel.PakReader;
using FModel.PakReader.IO;
using FModel.PakReader.Pak;
using FModel.PakReader.Parsers.Class;
using FModel.PakReader.Parsers.Objects;
namespace FModel.Utils
{
static class Assets
{
/// <summary>
/// used to cache assets
/// PakPackage to get the properties of the asset
/// ArraySegment<byte>[] to export the raw data
/// </summary>
private static readonly Dictionary<ReaderEntry, Dictionary<Package, ArraySegment<byte>[]>> _CachedFiles = new Dictionary<ReaderEntry, Dictionary<Package, ArraySegment<byte>[]>>();
private static Stopwatch _timer;
public static void ClearCachedFiles() => _CachedFiles.Clear();
/// <summary>
/// USER SELECTION ARRAY WILL BREAK AT THE FIRST ERROR
/// This won't happen for other type of extraction like in diff mode where we have to skip errors
/// </summary>
/// <param name="selection"></param>
/// <returns></returns>
public static async Task GetUserSelection(IList selection)
{
_timer = Stopwatch.StartNew();
ImageBoxVm.imageBoxViewModel.Reset();
AvalonEditVm.avalonEditViewModel.Reset();
ExtractStopVm.stopViewModel.IsEnabled = true;
ExtractStopVm.extractViewModel.IsEnabled = false;
StatusBarVm.statusBarViewModel.Set(string.Empty, Properties.Resources.Loading);
Tasks.TokenSource = new CancellationTokenSource();
await Task.Run(() =>
{
foreach (var item in selection)
{
if (Tasks.TokenSource.IsCancellationRequested)
throw new TaskCanceledException(Properties.Resources.Canceled);
Thread.Sleep(10); // this is actually useful because it smh unfreeze the ui so the user can cancel even tho it's a Task so...
if (item is ListBoxViewModel selected)
{
FFileIoStoreReader io = null;
if (Globals.CachedPakFiles.TryGetValue(selected.ReaderEntry.ContainerName, out var r) || Globals.CachedIoStores.TryGetValue(selected.ReaderEntry.ContainerName, out io))
{
string mount = r != null ? r.MountPoint : io!.MountPoint;
string ext = selected.ReaderEntry.GetExtension();
switch (ext)
{
case ".ini":
case ".txt":
case ".bat":
case ".xml":
case ".h":
case ".uproject":
case ".uplugin":
case ".upluginmanifest":
case ".csv":
case ".json":
{
IHighlightingDefinition syntax = ext switch
{
".ini" => AvalonEditVm.IniHighlighter,
".txt" => AvalonEditVm.IniHighlighter,
".bat" => AvalonEditVm.IniHighlighter,
".csv" => AvalonEditVm.IniHighlighter,
".xml" => AvalonEditVm.XmlHighlighter,
".h" => AvalonEditVm.CppHighlighter,
_ => AvalonEditVm.JsonHighlighter
};
using var asset = GetMemoryStream(selected.ReaderEntry.ContainerName, mount + selected.ReaderEntry.GetPathWithoutExtension());
asset.Position = 0;
using var reader = new StreamReader(asset);
AvalonEditVm.avalonEditViewModel.Set(reader.ReadToEnd(), mount + selected.ReaderEntry.Name, syntax);
break;
}
case ".locmeta":
{
using var asset = GetMemoryStream(selected.ReaderEntry.ContainerName, mount + selected.ReaderEntry.GetPathWithoutExtension());
asset.Position = 0;
AvalonEditVm.avalonEditViewModel.Set(JsonConvert.SerializeObject(new LocMetaReader(asset), Formatting.Indented), mount + selected.ReaderEntry.Name);
break;
}
case ".locres":
{
using var asset = GetMemoryStream(selected.ReaderEntry.ContainerName, mount + selected.ReaderEntry.GetPathWithoutExtension());
asset.Position = 0;
AvalonEditVm.avalonEditViewModel.Set(JsonConvert.SerializeObject(new LocResReader(asset).Entries, Formatting.Indented), mount + selected.ReaderEntry.Name);
break;
}
case ".udic":
{
using var asset = GetMemoryStream(selected.ReaderEntry.ContainerName, mount + selected.ReaderEntry.GetPathWithoutExtension());
asset.Position = 0;
AvalonEditVm.avalonEditViewModel.Set(JsonConvert.SerializeObject(new FOodleDictionaryArchive(asset).Header, Formatting.Indented), mount + selected.ReaderEntry.Name);
break;
}
case ".bin":
{
if (
!selected.ReaderEntry.Name.Equals("FortniteGame/AssetRegistry.bin") && // this file is 85mb...
selected.ReaderEntry.Name.Contains("AssetRegistry")) // only parse AssetRegistry (basically the ones in dynamic paks)
{
using var asset = GetMemoryStream(selected.ReaderEntry.ContainerName, mount + selected.ReaderEntry.GetPathWithoutExtension());
asset.Position = 0;
AvalonEditVm.avalonEditViewModel.Set(JsonConvert.SerializeObject(new FAssetRegistryState(asset), Formatting.Indented), mount + selected.ReaderEntry.Name);
}
break;
}
case ".bnk":
case ".pck":
{
using var asset = GetMemoryStream(selected.ReaderEntry.ContainerName, mount + selected.ReaderEntry.GetPathWithoutExtension());
asset.Position = 0;
WwiseReader bnk = new WwiseReader(new BinaryReader(asset));
Application.Current.Dispatcher.Invoke(delegate
{
DebugHelper.WriteLine("{0} {1} {2}", "[FModel]", "[Window]", $"Opening Audio Player for {selected.ReaderEntry.GetNameWithExtension()}");
if (!FWindows.IsWindowOpen<Window>(Properties.Resources.AudioPlayer))
new AudioPlayer().LoadFiles(bnk.AudioFiles, mount + selected.ReaderEntry.GetPathWithoutFile());
else
((AudioPlayer)FWindows.GetOpenedWindow<Window>(Properties.Resources.AudioPlayer)).LoadFiles(bnk.AudioFiles, mount + selected.ReaderEntry.GetPathWithoutFile());
});
break;
}
case ".png":
{
using var asset = GetMemoryStream(selected.ReaderEntry.ContainerName, mount + selected.ReaderEntry.GetPathWithoutExtension());
asset.Position = 0;
ImageBoxVm.imageBoxViewModel.Set(SKBitmap.Decode(asset), mount + selected.ReaderEntry.Name);
break;
}
case ".ushaderbytecode":
break;
default:
AvalonEditVm.avalonEditViewModel.Set(GetJsonProperties(selected.ReaderEntry, mount, true), mount + selected.ReaderEntry.Name);
break;
}
if (Properties.Settings.Default.AutoExport)
Export(selected.ReaderEntry, true);
}
}
}
}).ContinueWith(t =>
{
_timer.Stop();
ExtractStopVm.stopViewModel.IsEnabled = false;
ExtractStopVm.extractViewModel.IsEnabled = true;
if (t.Exception != null) Tasks.TaskCompleted(t.Exception);
else StatusBarVm.statusBarViewModel.Set(string.Format(Properties.Resources.TimeElapsed, _timer.ElapsedMilliseconds), Properties.Resources.Success);
},
TaskScheduler.FromCurrentSynchronizationContext());
}
public static MemoryStream GetMemoryStream(string pakName, string pathWithoutExtension)
{
if (Globals.CachedPakFiles.TryGetValue(pakName, out PakFileReader pak))
{
if (pak.Initialized && !pak.TryGetFile(pathWithoutExtension, out ArraySegment<byte> uasset, out _, out _))
{
if (uasset != null)
{
return new MemoryStream(uasset.Array, uasset.Offset, uasset.Count);
}
}
} else if (Globals.CachedIoStores.TryGetValue(pakName, out var ioStore))
{
if (ioStore.IsInitialized && ioStore.TryGetFile(pathWithoutExtension, out ArraySegment<byte> uasset, out _, out _))
{
if (uasset != null)
{
return new MemoryStream(uasset.Array, uasset.Offset, uasset.Count);
}
}
}
return null;
}
public static string GetJsonProperties(ReaderEntry entry, string mount) => GetJsonProperties(entry, mount, false);
public static string GetJsonProperties(ReaderEntry entry, string mount, bool loadContent)
{
Package p = GetPackage(entry, mount, loadContent);
if (!p.Equals(default))
{
return p.JsonData;
}
return string.Empty;
}
public static Package GetPackage(ReaderEntry entry, string mount) => GetPackage(entry, mount, false);
public static Package GetPackage(ReaderEntry entry, string mount, bool loadContent)
{
TryGetPackage(entry, mount, out var p);
if (loadContent)
{
// Texture
var i = p.GetExport<UTexture2D>();
if (i != null)
{
ImageBoxVm.imageBoxViewModel.Set(i.Image, entry.GetNameWithExtension());
return p;
}
var ak = p.GetExport<UAkMediaAssetData>();
if (ak != null)
{
if (Properties.Settings.Default.AutoOpenSounds)
{
Application.Current.Dispatcher.Invoke(delegate
{
DebugHelper.WriteLine("{0} {1} {2}", "[FModel]", "[Window]", $"Opening Audio Player for {entry.GetNameWithExtension()}");
if (!FWindows.IsWindowOpen<Window>(Properties.Resources.AudioPlayer))
new AudioPlayer().LoadFile(ak.Sound, entry.GetNameWithoutExtension() + ".wem", mount + entry.GetPathWithoutFile());
else
((AudioPlayer)FWindows.GetOpenedWindow<Window>(Properties.Resources.AudioPlayer)).LoadFile(ak.Sound, entry.GetNameWithoutExtension() + ".wem", mount + entry.GetPathWithoutFile());
});
}
return p;
}
// Sound
var s = p.GetExport<USoundWave>();
if (s != null && (s.AudioFormat.String.Equals("OGG") || s.AudioFormat.String.Equals("OGG10000-1-1-1-1-1") || s.AudioFormat.String.Equals("OGG10025600-1-1-1-1-1")))
{
string path = Properties.Settings.Default.OutputPath + "\\Sounds\\" + mount + entry.GetPathWithoutExtension() + ".ogg";
Directory.CreateDirectory(Path.GetDirectoryName(path));
if (File.Exists(path))
{
if (!Paks.IsFileWriteLocked(new FileInfo(path))) // aka isn't already being played, rewrite it
{
using var stream = new FileStream(path, FileMode.Create, FileAccess.Write);
using var writer = new BinaryWriter(stream);
writer.Write(s.Sound);
writer.Flush();
}
}
else
{
using var stream = new FileStream(path, FileMode.Create, FileAccess.Write);
using var writer = new BinaryWriter(stream);
writer.Write(s.Sound);
writer.Flush();
}
if (Properties.Settings.Default.AutoOpenSounds)
{
Application.Current.Dispatcher.Invoke(delegate
{
DebugHelper.WriteLine("{0} {1} {2}", "[FModel]", "[Window]", $"Opening Audio Player for {entry.GetNameWithExtension()}");
if (!FWindows.IsWindowOpen<Window>(Properties.Resources.AudioPlayer))
new AudioPlayer().LoadFile(path);
else
((AudioPlayer)FWindows.GetOpenedWindow<Window>(Properties.Resources.AudioPlayer)).LoadFile(path);
});
}
return p;
}
else if (s != null && (s.AudioFormat.String.Equals("OPUS") || s.AudioFormat.String.Equals("ADPCM")))
{
Application.Current.Dispatcher.Invoke(delegate
{
DebugHelper.WriteLine("{0} {1} {2}", "[FModel]", "[Window]", $"Opening Audio Player for {entry.GetNameWithExtension()}");
if (!FWindows.IsWindowOpen<Window>(Properties.Resources.AudioPlayer))
new AudioPlayer().LoadFiles(new Dictionary<string, byte[]>(1) {{ entry.GetNameWithoutExtension() + "." + s.AudioFormat.String.ToLowerInvariant(), s.Sound }}, entry.GetPathWithoutFile());
else
((AudioPlayer)FWindows.GetOpenedWindow<Window>(Properties.Resources.AudioPlayer)).LoadFiles(new Dictionary<string, byte[]>(1) { { entry.GetNameWithoutExtension() + "." + s.AudioFormat.String.ToLowerInvariant(), s.Sound } }, entry.GetPathWithoutFile());
});
}
else if (s != null)
{
string path = Properties.Settings.Default.OutputPath + "\\Sounds\\" + mount + entry.GetPathWithoutExtension() + "." + s.AudioFormat.String.ToLowerInvariant();
Directory.CreateDirectory(Path.GetDirectoryName(path));
using var stream = new FileStream(path, FileMode.Create, FileAccess.Write);
using var writer = new BinaryWriter(stream);
writer.Write(s.Sound);
writer.Flush();
return p;
}
// Image Creator
if (TryDrawIcon(entry.Name, p.ExportTypes, p.Exports))
return p;
}
return p;
}
private static bool TryGetPackage(ReaderEntry entry, string mount, out Package package)
{
DebugHelper.WriteLine("{0} {1} {2} {3}", "[FModel]", "[Assets]", "[Package]", $"Searching for '{mount + entry.Name}'s package");
if (_CachedFiles.TryGetValue(entry, out var dict))
{
package = dict.ElementAt(0).Key;
return true;
}
if (Globals.CachedPakFiles.TryGetValue(entry.ContainerName, out PakFileReader pak))
{
if (pak.Initialized && pak.TryGetFile(mount + entry.GetPathWithoutExtension(), out ArraySegment<byte> uasset, out ArraySegment<byte> uexp, out ArraySegment<byte> ubulk))
{
package = new PakPackage(uasset, uexp, ubulk);
_CachedFiles[entry] = new Dictionary<Package, ArraySegment<byte>[]>
{
[package] = new ArraySegment<byte>[] { uasset, uexp, ubulk }
};
return true;
}
}
else if (entry is FIoStoreEntry ioStoreEntry)
{
var uasset = ioStoreEntry.GetData();
var uexp = (ioStoreEntry.Uexp as FIoStoreEntry)?.GetData();
var ubulk = (ioStoreEntry.Ubulk as FIoStoreEntry)?.GetData();
if (uexp != null)
package = new PakPackage(uasset, uexp, ubulk);
else
package = new IoPackage(uasset, ubulk);
#if !DEBUG
_CachedFiles[entry] = new Dictionary<Package, ArraySegment<byte>[]>
{
[package] = new ArraySegment<byte>[] { uasset, uexp, ubulk }
};
#endif
return true;
}
DebugHelper.WriteLine("{0} {1} {2} {3}", "[FModel]", "[Assets]", "[Package]", $"No package found for '{mount + entry.Name}'");
package = default;
return false;
}
public static ArraySegment<byte>[] GetArraySegmentByte(ReaderEntry entry, string mount)
{
TryGetArraySegmentByte(entry, mount, out var b);
return b;
}
private static bool TryGetArraySegmentByte(ReaderEntry entry, string mount, out ArraySegment<byte>[] arraySegment)
{
DebugHelper.WriteLine("{0} {1} {2} {3}", "[FModel]", "[Assets]", "[ArraySegment]", $"Searching for '{mount + entry.Name}'s ArraySegment<byte>");
if (_CachedFiles.TryGetValue(entry, out var dict))
{
arraySegment = dict.ElementAt(0).Value;
return true;
}
if (Globals.CachedPakFiles.TryGetValue(entry.ContainerName, out PakFileReader pak))
{
if (pak.Initialized && pak.TryGetFile(mount + entry.GetPathWithoutExtension(), out ArraySegment<byte> uasset, out ArraySegment<byte> uexp, out ArraySegment<byte> ubulk))
{
arraySegment = new ArraySegment<byte>[] { uasset, uexp, ubulk };
return true;
}
} else if (entry is FIoStoreEntry ioStoreEntry)
{
var uasset = ioStoreEntry.GetData();
var uexp = (ioStoreEntry.Uexp as FIoStoreEntry)?.GetData();
var ubulk = (ioStoreEntry.Ubulk as FIoStoreEntry)?.GetData();
arraySegment = new ArraySegment<byte>[] { uasset, uexp, ubulk };
return true;
}
DebugHelper.WriteLine("{0} {1} {2} {3}", "[FModel]", "[Assets]", "[ArraySegment]", $"No ArraySegment<byte> found for '{mount + entry.Name}'");
arraySegment = default;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Filter(string filter, string item, out bool bSearch)
{
if (filter.StartsWith("!="))
bSearch = item.IndexOf(filter[2..], StringComparison.CurrentCultureIgnoreCase) < 0;
else if (filter.StartsWith("=="))
bSearch = item.IndexOf(filter[2..], StringComparison.CurrentCulture) >= 0;
else
bSearch = item.IndexOf(filter, StringComparison.CurrentCultureIgnoreCase) >= 0;
}
public static void Export(ReaderEntry entry, bool autoSave)
{
switch (entry.GetExtension())
{
case ".uasset": // embedded data export
case ".umap": // embedded data export
{
FFileIoStoreReader io = null;
if (Globals.CachedPakFiles.TryGetValue(entry.ContainerName, out var r) || Globals.CachedIoStores.TryGetValue(entry.ContainerName, out io))
{
string mount = r != null ? r.MountPoint : io!.MountPoint;
if (TryGetArraySegmentByte(entry, mount, out var data))
{
string[] ext = string.Join(":", entry.GetExtension(), entry.Uexp?.GetExtension(), entry.Ubulk?.GetExtension()).Split(':');
for (int i = 0; i < data.Length; i++)
{
if (data[i] == null)
continue;
string basePath = Properties.Settings.Default.OutputPath + "\\Exports\\" + mount[1..];
string fullPath = basePath + Path.ChangeExtension(entry.Name, ext[i]);
string name = Path.GetFileName(fullPath);
Directory.CreateDirectory(basePath + entry.GetPathWithoutFile());
using var stream = new FileStream(fullPath, FileMode.Create, FileAccess.Write);
using var writer = new BinaryWriter(stream);
writer.Write(data[i]);
writer.Flush();
if (File.Exists(fullPath))
{
DebugHelper.WriteLine("{0} {1} {2}", "[FModel]", "[Assets]", $"{name} successfully exported");
if (autoSave)
FConsole.AppendText(string.Format(Properties.Resources.DataExported, name), FColors.Green, true);
else
Globals.gNotifier.ShowCustomMessage(Properties.Resources.Success, string.Format(Properties.Resources.DataExported, name), string.Empty, fullPath);
}
}
}
}
break;
}
default: // single data export
{
FFileIoStoreReader io = null;
if (Globals.CachedPakFiles.TryGetValue(entry.ContainerName, out var r) || Globals.CachedIoStores.TryGetValue(entry.ContainerName, out io))
{
string mount = r != null ? r.MountPoint : io!.MountPoint;
string basePath = Properties.Settings.Default.OutputPath + "\\Exports\\" + mount[1..];
string fullPath = basePath + entry.Name;
string name = Path.GetFileName(fullPath);
Directory.CreateDirectory(basePath + entry.GetPathWithoutFile());
using var data = GetMemoryStream(entry.ContainerName, mount + entry.GetPathWithoutExtension());
using var stream = new FileStream(fullPath, FileMode.Create, FileAccess.Write);
data.WriteTo(stream);
if (File.Exists(fullPath))
{
DebugHelper.WriteLine("{0} {1} {2}", "[FModel]", "[Assets]", $"{name} successfully exported");
if (autoSave)
FConsole.AppendText(string.Format(Properties.Resources.DataExported, name), FColors.Green, true);
else
Globals.gNotifier.ShowCustomMessage(Properties.Resources.Success, string.Format(Properties.Resources.DataExported, name, string.Empty, fullPath));
}
} else {}
break;
}
}
}
public static void Copy(IList entries, ECopy mode)
{
StringBuilder sb = new StringBuilder();
if (entries[0] is ListBoxViewModel)
{
foreach (ListBoxViewModel selectedItem in entries)
{
sb.AppendLine(Copy(selectedItem.ReaderEntry, mode));
}
}
else if (entries[0] is DataGridViewModel)
{
foreach (DataGridViewModel selectedItem in entries)
{
sb.AppendLine(Copy(selectedItem.Name, mode));
}
}
Copy(sb.ToString().Trim());
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Copy(ReaderEntry entry, ECopy mode)
{
FFileIoStoreReader io = null;
if (Globals.CachedPakFiles.TryGetValue(entry.ContainerName, out var r) || Globals.CachedIoStores.TryGetValue(entry.ContainerName, out io))
{
string toCopy;
if (r != null)
toCopy = r.MountPoint.Substring(1);
else
toCopy = io!.MountPoint[1..];
if (mode == ECopy.Path)
toCopy += entry.Name;
else if (mode == ECopy.PathNoExt)
toCopy += entry.GetPathWithoutExtension();
else if (mode == ECopy.PathNoFile)
toCopy += entry.GetPathWithoutFile();
else if (mode == ECopy.File)
toCopy = entry.GetNameWithExtension();
else if (mode == ECopy.FileNoExt)
toCopy = entry.GetNameWithoutExtension();
return toCopy;
}
return string.Empty;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Copy(string fullPath, ECopy mode)
{
string toCopy = string.Empty;
if (mode == ECopy.Path)
toCopy = fullPath;
else if (mode == ECopy.PathNoExt)
toCopy = fullPath.Substring(0, fullPath.LastIndexOf("."));
else if (mode == ECopy.PathNoFile)
toCopy = fullPath.Substring(0, fullPath.LastIndexOf("/") + 1);
else if (mode == ECopy.File)
toCopy = fullPath.Substring(fullPath.LastIndexOf("/") + 1);
else if (mode == ECopy.FileNoExt)
toCopy = fullPath.Substring(fullPath.LastIndexOf("/") + 1, fullPath.LastIndexOf(".") - (fullPath.LastIndexOf("/") + 1));
return toCopy;
}
public static void Copy(string toCopy)
{
Clipboard.SetText(toCopy);
if (Clipboard.GetText().Equals(toCopy))
Globals.gNotifier.ShowCustomMessage(Properties.Resources.Success, Properties.Resources.CopySuccess, "/FModel;component/Resources/check-circle.ico");
}
}
}