mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +01:00
193 lines
6.8 KiB
C#
193 lines
6.8 KiB
C#
using Newtonsoft.Json;
|
|
using OtterGui.Compression;
|
|
using Penumbra.Import.Structs;
|
|
using Penumbra.Mods.Editor;
|
|
using Penumbra.Mods.Manager;
|
|
using FileMode = System.IO.FileMode;
|
|
using ZipArchive = SharpCompress.Archives.Zip.ZipArchive;
|
|
using ZipArchiveEntry = SharpCompress.Archives.Zip.ZipArchiveEntry;
|
|
|
|
namespace Penumbra.Import;
|
|
|
|
public partial class TexToolsImporter : IDisposable
|
|
{
|
|
private const string TempFileName = "textools-import";
|
|
private static readonly JsonSerializerSettings JsonSettings = new() { NullValueHandling = NullValueHandling.Ignore };
|
|
|
|
private readonly DirectoryInfo _baseDirectory;
|
|
private readonly string _tmpFile;
|
|
|
|
private readonly IEnumerable<FileInfo> _modPackFiles;
|
|
private readonly int _modPackCount;
|
|
private FileStream? _tmpFileStream;
|
|
private StreamDisposer? _streamDisposer;
|
|
private readonly CancellationTokenSource _cancellation = new();
|
|
private readonly CancellationToken _token;
|
|
|
|
public ImporterState State { get; private set; }
|
|
public readonly List<(FileInfo File, DirectoryInfo? Mod, Exception? Error)> ExtractedMods;
|
|
|
|
private readonly Configuration _config;
|
|
private readonly ModEditor _editor;
|
|
private readonly ModManager _modManager;
|
|
private readonly FileCompactor _compactor;
|
|
|
|
public TexToolsImporter(int count, IEnumerable<FileInfo> modPackFiles, Action<FileInfo, DirectoryInfo?, Exception?> handler,
|
|
Configuration config, ModEditor editor, ModManager modManager, FileCompactor compactor)
|
|
{
|
|
_baseDirectory = modManager.BasePath;
|
|
_tmpFile = Path.Combine(_baseDirectory.FullName, TempFileName);
|
|
_modPackFiles = modPackFiles;
|
|
_config = config;
|
|
_editor = editor;
|
|
_modManager = modManager;
|
|
_compactor = compactor;
|
|
_modPackCount = count;
|
|
ExtractedMods = new List<(FileInfo, DirectoryInfo?, Exception?)>(count);
|
|
_token = _cancellation.Token;
|
|
Task.Run(ImportFiles, _token)
|
|
.ContinueWith(_ => CloseStreams())
|
|
.ContinueWith(_ =>
|
|
{
|
|
foreach (var (file, dir, error) in ExtractedMods)
|
|
handler(file, dir, error);
|
|
});
|
|
}
|
|
|
|
private void CloseStreams()
|
|
{
|
|
_tmpFileStream?.Dispose();
|
|
_tmpFileStream = null;
|
|
ResetStreamDisposer();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_cancellation.Cancel(true);
|
|
if (State != ImporterState.WritingPackToDisk)
|
|
{
|
|
_tmpFileStream?.Dispose();
|
|
_tmpFileStream = null;
|
|
}
|
|
|
|
if (State != ImporterState.ExtractingModFiles)
|
|
ResetStreamDisposer();
|
|
}
|
|
|
|
private void ImportFiles()
|
|
{
|
|
State = ImporterState.None;
|
|
_currentModPackIdx = 0;
|
|
foreach (var file in _modPackFiles)
|
|
{
|
|
_currentModDirectory = null;
|
|
if (_token.IsCancellationRequested)
|
|
{
|
|
ExtractedMods.Add((file, null, new TaskCanceledException("Task canceled by user.")));
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
var directory = VerifyVersionAndImport(file);
|
|
ExtractedMods.Add((file, directory, null));
|
|
if (_config.AutoDeduplicateOnImport)
|
|
{
|
|
State = ImporterState.DeduplicatingFiles;
|
|
_editor.Duplicates.DeduplicateMod(directory);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
ExtractedMods.Add((file, _currentModDirectory, e));
|
|
_currentNumOptions = 0;
|
|
_currentOptionIdx = 0;
|
|
_currentFileIdx = 0;
|
|
_currentNumFiles = 0;
|
|
}
|
|
|
|
++_currentModPackIdx;
|
|
}
|
|
|
|
State = ImporterState.Done;
|
|
}
|
|
|
|
// Rudimentary analysis of a TTMP file by extension and version.
|
|
// Puts out warnings if extension does not correspond to data.
|
|
private DirectoryInfo VerifyVersionAndImport(FileInfo modPackFile)
|
|
{
|
|
if (modPackFile.Extension.ToLowerInvariant() is ".pmp" or ".zip" or ".7z" or ".rar")
|
|
return HandleRegularArchive(modPackFile);
|
|
|
|
using var zfs = modPackFile.OpenRead();
|
|
using var extractedModPack = ZipArchive.Open(zfs);
|
|
|
|
var mpl = FindZipEntry(extractedModPack, "TTMPL.mpl");
|
|
if (mpl == null)
|
|
throw new FileNotFoundException("ZIP does not contain a TTMPL.mpl file.");
|
|
|
|
var modRaw = GetStringFromZipEntry(mpl, Encoding.UTF8);
|
|
|
|
// At least a better validation than going by the extension.
|
|
if (modRaw.Contains("\"TTMPVersion\":"))
|
|
{
|
|
if (modPackFile.Extension != ".ttmp2")
|
|
Penumbra.Log.Warning($"File {modPackFile.FullName} seems to be a V2 TTMP, but has the wrong extension.");
|
|
|
|
return ImportV2ModPack(modPackFile, extractedModPack, modRaw);
|
|
}
|
|
|
|
if (modPackFile.Extension != ".ttmp")
|
|
Penumbra.Log.Warning($"File {modPackFile.FullName} seems to be a V1 TTMP, but has the wrong extension.");
|
|
|
|
return ImportV1ModPack(modPackFile, extractedModPack, modRaw);
|
|
}
|
|
|
|
// You can in no way rely on any file paths in TTMPs so we need to just do this, sorry
|
|
private static ZipArchiveEntry? FindZipEntry(ZipArchive file, string fileName)
|
|
=> file.Entries.FirstOrDefault(e => !e.IsDirectory && e.Key.Contains(fileName));
|
|
|
|
private static string GetStringFromZipEntry(ZipArchiveEntry entry, Encoding encoding)
|
|
{
|
|
using var ms = new MemoryStream();
|
|
using var s = entry.OpenEntryStream();
|
|
s.CopyTo(ms);
|
|
return encoding.GetString(ms.ToArray());
|
|
}
|
|
|
|
private void WriteZipEntryToTempFile(Stream s)
|
|
{
|
|
_tmpFileStream?.Dispose(); // should not happen
|
|
_tmpFileStream = new FileStream(_tmpFile, FileMode.Create);
|
|
if (_token.IsCancellationRequested)
|
|
return;
|
|
|
|
s.CopyTo(_tmpFileStream);
|
|
_tmpFileStream.Dispose();
|
|
_tmpFileStream = null;
|
|
}
|
|
|
|
private StreamDisposer GetSqPackStreamStream(ZipArchive file, string entryName)
|
|
{
|
|
State = ImporterState.WritingPackToDisk;
|
|
|
|
// write shitty zip garbage to disk
|
|
var entry = FindZipEntry(file, entryName);
|
|
if (entry == null)
|
|
throw new FileNotFoundException($"ZIP does not contain a file named {entryName}.");
|
|
|
|
using var s = entry.OpenEntryStream();
|
|
|
|
WriteZipEntryToTempFile(s);
|
|
|
|
_streamDisposer?.Dispose(); // Should not happen.
|
|
var fs = new FileStream(_tmpFile, FileMode.Open);
|
|
return new StreamDisposer(fs);
|
|
}
|
|
|
|
private void ResetStreamDisposer()
|
|
{
|
|
_streamDisposer?.Dispose();
|
|
_streamDisposer = null;
|
|
}
|
|
}
|