mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-13 12:14:17 +01:00
Add some migration things.
This commit is contained in:
parent
0d939b12f4
commit
56e284a99e
17 changed files with 515 additions and 80 deletions
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
||||||
Subproject commit c2738e1d42974cddbe5a31238c6ed236a831d17d
|
Subproject commit 89b3b9513f9b4989045517a452ef971e24377203
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 62f6acfb0f9e31b2907c02a270d723bfff18b390
|
Subproject commit b5eb074d80a4aaa2703a3124d2973a7fa08046ad
|
||||||
|
|
@ -99,6 +99,8 @@ public class Configuration : IPluginConfiguration, ISavable, IService
|
||||||
public bool UseFileSystemCompression { get; set; } = true;
|
public bool UseFileSystemCompression { get; set; } = true;
|
||||||
public bool EnableHttpApi { get; set; } = true;
|
public bool EnableHttpApi { get; set; } = true;
|
||||||
|
|
||||||
|
public bool MigrateImportedModelsToV6 { get; set; } = false;
|
||||||
|
|
||||||
public string DefaultModImportPath { get; set; } = string.Empty;
|
public string DefaultModImportPath { get; set; } = string.Empty;
|
||||||
public bool AlwaysOpenDefaultImport { get; set; } = false;
|
public bool AlwaysOpenDefaultImport { get; set; } = false;
|
||||||
public bool KeepDefaultMetaChanges { get; set; } = false;
|
public bool KeepDefaultMetaChanges { get; set; } = false;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using OtterGui.Compression;
|
||||||
using Penumbra.Import.Structs;
|
using Penumbra.Import.Structs;
|
||||||
using Penumbra.Mods.Editor;
|
using Penumbra.Mods.Editor;
|
||||||
using Penumbra.Mods.Manager;
|
using Penumbra.Mods.Manager;
|
||||||
|
using Penumbra.Services;
|
||||||
using FileMode = System.IO.FileMode;
|
using FileMode = System.IO.FileMode;
|
||||||
using ZipArchive = SharpCompress.Archives.Zip.ZipArchive;
|
using ZipArchive = SharpCompress.Archives.Zip.ZipArchive;
|
||||||
using ZipArchiveEntry = SharpCompress.Archives.Zip.ZipArchiveEntry;
|
using ZipArchiveEntry = SharpCompress.Archives.Zip.ZipArchiveEntry;
|
||||||
|
|
@ -27,24 +28,26 @@ public partial class TexToolsImporter : IDisposable
|
||||||
public ImporterState State { get; private set; }
|
public ImporterState State { get; private set; }
|
||||||
public readonly List<(FileInfo File, DirectoryInfo? Mod, Exception? Error)> ExtractedMods;
|
public readonly List<(FileInfo File, DirectoryInfo? Mod, Exception? Error)> ExtractedMods;
|
||||||
|
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly ModEditor _editor;
|
private readonly ModEditor _editor;
|
||||||
private readonly ModManager _modManager;
|
private readonly ModManager _modManager;
|
||||||
private readonly FileCompactor _compactor;
|
private readonly FileCompactor _compactor;
|
||||||
|
private readonly MigrationManager _migrationManager;
|
||||||
|
|
||||||
public TexToolsImporter(int count, IEnumerable<FileInfo> modPackFiles, Action<FileInfo, DirectoryInfo?, Exception?> handler,
|
public TexToolsImporter(int count, IEnumerable<FileInfo> modPackFiles, Action<FileInfo, DirectoryInfo?, Exception?> handler,
|
||||||
Configuration config, ModEditor editor, ModManager modManager, FileCompactor compactor)
|
Configuration config, ModEditor editor, ModManager modManager, FileCompactor compactor, MigrationManager migrationManager)
|
||||||
{
|
{
|
||||||
_baseDirectory = modManager.BasePath;
|
_baseDirectory = modManager.BasePath;
|
||||||
_tmpFile = Path.Combine(_baseDirectory.FullName, TempFileName);
|
_tmpFile = Path.Combine(_baseDirectory.FullName, TempFileName);
|
||||||
_modPackFiles = modPackFiles;
|
_modPackFiles = modPackFiles;
|
||||||
_config = config;
|
_config = config;
|
||||||
_editor = editor;
|
_editor = editor;
|
||||||
_modManager = modManager;
|
_modManager = modManager;
|
||||||
_compactor = compactor;
|
_compactor = compactor;
|
||||||
_modPackCount = count;
|
_migrationManager = migrationManager;
|
||||||
ExtractedMods = new List<(FileInfo, DirectoryInfo?, Exception?)>(count);
|
_modPackCount = count;
|
||||||
_token = _cancellation.Token;
|
ExtractedMods = new List<(FileInfo, DirectoryInfo?, Exception?)>(count);
|
||||||
|
_token = _cancellation.Token;
|
||||||
Task.Run(ImportFiles, _token)
|
Task.Run(ImportFiles, _token)
|
||||||
.ContinueWith(_ => CloseStreams(), TaskScheduler.Default)
|
.ContinueWith(_ => CloseStreams(), TaskScheduler.Default)
|
||||||
.ContinueWith(_ =>
|
.ContinueWith(_ =>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using Dalamud.Utility;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using OtterGui.Filesystem;
|
using OtterGui.Filesystem;
|
||||||
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.Import.Structs;
|
using Penumbra.Import.Structs;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
using SharpCompress.Archives;
|
using SharpCompress.Archives;
|
||||||
|
|
@ -9,12 +10,19 @@ using SharpCompress.Archives.Rar;
|
||||||
using SharpCompress.Archives.SevenZip;
|
using SharpCompress.Archives.SevenZip;
|
||||||
using SharpCompress.Common;
|
using SharpCompress.Common;
|
||||||
using SharpCompress.Readers;
|
using SharpCompress.Readers;
|
||||||
|
using static FFXIVClientStructs.FFXIV.Client.UI.Misc.ConfigModule;
|
||||||
using ZipArchive = SharpCompress.Archives.Zip.ZipArchive;
|
using ZipArchive = SharpCompress.Archives.Zip.ZipArchive;
|
||||||
|
|
||||||
namespace Penumbra.Import;
|
namespace Penumbra.Import;
|
||||||
|
|
||||||
public partial class TexToolsImporter
|
public partial class TexToolsImporter
|
||||||
{
|
{
|
||||||
|
private static readonly ExtractionOptions _extractionOptions = new()
|
||||||
|
{
|
||||||
|
ExtractFullPath = true,
|
||||||
|
Overwrite = true,
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extract regular compressed archives that are folders containing penumbra-formatted mods.
|
/// Extract regular compressed archives that are folders containing penumbra-formatted mods.
|
||||||
/// The mod has to either contain a meta.json at top level, or one folder deep.
|
/// The mod has to either contain a meta.json at top level, or one folder deep.
|
||||||
|
|
@ -45,11 +53,7 @@ public partial class TexToolsImporter
|
||||||
Penumbra.Log.Information($" -> Importing {archive.Type} Archive.");
|
Penumbra.Log.Information($" -> Importing {archive.Type} Archive.");
|
||||||
|
|
||||||
_currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, Path.GetRandomFileName(), _config.ReplaceNonAsciiOnImport, true);
|
_currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, Path.GetRandomFileName(), _config.ReplaceNonAsciiOnImport, true);
|
||||||
var options = new ExtractionOptions()
|
|
||||||
{
|
|
||||||
ExtractFullPath = true,
|
|
||||||
Overwrite = true,
|
|
||||||
};
|
|
||||||
|
|
||||||
State = ImporterState.ExtractingModFiles;
|
State = ImporterState.ExtractingModFiles;
|
||||||
_currentFileIdx = 0;
|
_currentFileIdx = 0;
|
||||||
|
|
@ -86,7 +90,7 @@ public partial class TexToolsImporter
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
reader.WriteEntryToDirectory(_currentModDirectory.FullName, options);
|
HandleFileMigrations(reader);
|
||||||
}
|
}
|
||||||
|
|
||||||
++_currentFileIdx;
|
++_currentFileIdx;
|
||||||
|
|
@ -114,6 +118,16 @@ public partial class TexToolsImporter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void HandleFileMigrations(IReader reader)
|
||||||
|
{
|
||||||
|
switch (Path.GetExtension(reader.Entry.Key))
|
||||||
|
{
|
||||||
|
case ".mdl":
|
||||||
|
_migrationManager.MigrateMdlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Search the archive for the meta.json file which needs to exist.
|
// Search the archive for the meta.json file which needs to exist.
|
||||||
private static string FindArchiveModMeta(IArchive archive, out bool leadDir)
|
private static string FindArchiveModMeta(IArchive archive, out bool leadDir)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -253,25 +253,12 @@ public partial class TexToolsImporter
|
||||||
|
|
||||||
extractedFile.Directory?.Create();
|
extractedFile.Directory?.Create();
|
||||||
|
|
||||||
if (extractedFile.FullName.EndsWith(".mdl"))
|
data.Data = Path.GetExtension(extractedFile.FullName) switch
|
||||||
ProcessMdl(data.Data);
|
{
|
||||||
|
".mdl" => _migrationManager.MigrateTtmpModel(extractedFile.FullName, data.Data),
|
||||||
|
_ => data.Data,
|
||||||
|
};
|
||||||
|
|
||||||
_compactor.WriteAllBytesAsync(extractedFile.FullName, data.Data, _token).Wait(_token);
|
_compactor.WriteAllBytesAsync(extractedFile.FullName, data.Data, _token).Wait(_token);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ProcessMdl(byte[] mdl)
|
|
||||||
{
|
|
||||||
const int modelHeaderLodOffset = 22;
|
|
||||||
|
|
||||||
// Model file header LOD num
|
|
||||||
mdl[64] = 1;
|
|
||||||
|
|
||||||
// Model header LOD num
|
|
||||||
var stackSize = BitConverter.ToUInt32(mdl, 4);
|
|
||||||
var runtimeBegin = stackSize + 0x44;
|
|
||||||
var stringsLengthOffset = runtimeBegin + 4;
|
|
||||||
var stringsLength = BitConverter.ToUInt32(mdl, (int)stringsLengthOffset);
|
|
||||||
var modelHeaderStart = stringsLengthOffset + stringsLength + 4;
|
|
||||||
mdl[modelHeaderStart + modelHeaderLodOffset] = 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ public sealed unsafe class LoadAreaVfx : FastHook<LoadAreaVfx.Delegate>
|
||||||
var last = _state.SetAnimationData(newData);
|
var last = _state.SetAnimationData(newData);
|
||||||
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadAreaVfx);
|
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadAreaVfx);
|
||||||
var ret = Task.Result.Original(vfxId, pos, caster, unk1, unk2, unk3);
|
var ret = Task.Result.Original(vfxId, pos, caster, unk1, unk2, unk3);
|
||||||
Penumbra.Log.Information(
|
Penumbra.Log.Excessive(
|
||||||
$"[Load Area VFX] Invoked with {vfxId}, [{pos[0]} {pos[1]} {pos[2]}], 0x{(nint)caster:X}, {unk1}, {unk2}, {unk3} -> 0x{ret:X}.");
|
$"[Load Area VFX] Invoked with {vfxId}, [{pos[0]} {pos[1]} {pos[2]}], 0x{(nint)caster:X}, {unk1}, {unk2}, {unk3} -> 0x{ret:X}.");
|
||||||
_state.RestoreAnimationData(last);
|
_state.RestoreAnimationData(last);
|
||||||
return ret;
|
return ret;
|
||||||
|
|
|
||||||
|
|
@ -94,9 +94,6 @@ public unsafe class ResourceLoader : IDisposable, IService
|
||||||
|
|
||||||
CompareHash(ComputeHash(path.Path, parameters), hash, path);
|
CompareHash(ComputeHash(path.Path, parameters), hash, path);
|
||||||
|
|
||||||
if (path.ToString() == "vfx/common/eff/abi_cnj022g.avfx")
|
|
||||||
;
|
|
||||||
|
|
||||||
// If no replacements are being made, we still want to be able to trigger the event.
|
// If no replacements are being made, we still want to be able to trigger the event.
|
||||||
var (resolvedPath, data) = _incMode.Value
|
var (resolvedPath, data) = _incMode.Value
|
||||||
? (null, ResolveData.Invalid)
|
? (null, ResolveData.Invalid)
|
||||||
|
|
|
||||||
|
|
@ -81,11 +81,12 @@ internal partial record ResolveContext
|
||||||
// Resolving a material path through the game's code can dereference null pointers for materials that involve IMC metadata.
|
// Resolving a material path through the game's code can dereference null pointers for materials that involve IMC metadata.
|
||||||
return ModelType switch
|
return ModelType switch
|
||||||
{
|
{
|
||||||
ModelType.Human when SlotIndex < 10 && mtrlFileName[8] != (byte)'b' => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
|
ModelType.Human when SlotIndex is < 10 or 16 && mtrlFileName[8] != (byte)'b'
|
||||||
ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
|
=> ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
|
||||||
ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName),
|
ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
|
||||||
ModelType.Monster => ResolveMonsterMaterialPath(modelPath, imc, mtrlFileName),
|
ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName),
|
||||||
_ => ResolveMaterialPathNative(mtrlFileName),
|
ModelType.Monster => ResolveMonsterMaterialPath(modelPath, imc, mtrlFileName),
|
||||||
|
_ => ResolveMaterialPathNative(mtrlFileName),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,7 +97,7 @@ internal partial record ResolveContext
|
||||||
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
|
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
|
||||||
|
|
||||||
Span<byte> pathBuffer = stackalloc byte[260];
|
Span<byte> pathBuffer = stackalloc byte[260];
|
||||||
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName);
|
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName);
|
||||||
|
|
||||||
return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty;
|
return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty;
|
||||||
}
|
}
|
||||||
|
|
@ -126,7 +127,7 @@ internal partial record ResolveContext
|
||||||
WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId);
|
WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId);
|
||||||
|
|
||||||
Span<byte> pathBuffer = stackalloc byte[260];
|
Span<byte> pathBuffer = stackalloc byte[260];
|
||||||
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName);
|
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName);
|
||||||
|
|
||||||
var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8);
|
var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8);
|
||||||
if (weaponPosition >= 0)
|
if (weaponPosition >= 0)
|
||||||
|
|
@ -145,7 +146,7 @@ internal partial record ResolveContext
|
||||||
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
|
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
|
||||||
|
|
||||||
Span<byte> pathBuffer = stackalloc byte[260];
|
Span<byte> pathBuffer = stackalloc byte[260];
|
||||||
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName);
|
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName);
|
||||||
|
|
||||||
return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty;
|
return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty;
|
||||||
}
|
}
|
||||||
|
|
@ -166,7 +167,8 @@ internal partial record ResolveContext
|
||||||
return entry.MaterialId;
|
return entry.MaterialId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Span<byte> AssembleMaterialPath(Span<byte> materialPathBuffer, ReadOnlySpan<byte> modelPath, byte variant, ReadOnlySpan<byte> mtrlFileName)
|
private static Span<byte> AssembleMaterialPath(Span<byte> materialPathBuffer, ReadOnlySpan<byte> modelPath, byte variant,
|
||||||
|
ReadOnlySpan<byte> mtrlFileName)
|
||||||
{
|
{
|
||||||
var modelPosition = modelPath.IndexOf("/model/"u8);
|
var modelPosition = modelPath.IndexOf("/model/"u8);
|
||||||
if (modelPosition < 0)
|
if (modelPosition < 0)
|
||||||
|
|
@ -187,8 +189,8 @@ internal partial record ResolveContext
|
||||||
{
|
{
|
||||||
for (var i = destination.Length; i-- > 0;)
|
for (var i = destination.Length; i-- > 0;)
|
||||||
{
|
{
|
||||||
destination[i] = (byte)('0' + number % 10);
|
destination[i] = (byte)('0' + number % 10);
|
||||||
number /= 10;
|
number /= 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,13 +199,17 @@ internal partial record ResolveContext
|
||||||
ByteString? path;
|
ByteString? path;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
Penumbra.Log.Information($"{(nint)CharacterBase:X} {ModelType} {SlotIndex} 0x{(ulong)mtrlFileName:X}");
|
||||||
|
Penumbra.Log.Information($"{new ByteString(mtrlFileName)}");
|
||||||
path = CharacterBase->ResolveMtrlPathAsByteString(SlotIndex, mtrlFileName);
|
path = CharacterBase->ResolveMtrlPathAsByteString(SlotIndex, mtrlFileName);
|
||||||
}
|
}
|
||||||
catch (AccessViolationException)
|
catch (AccessViolationException)
|
||||||
{
|
{
|
||||||
Penumbra.Log.Error($"Access violation during attempt to resolve material path\nDraw object: {(nint)CharacterBase:X} (of type {ModelType})\nSlot index: {SlotIndex}\nMaterial file name: {(nint)mtrlFileName:X} ({new string((sbyte*)mtrlFileName)})");
|
Penumbra.Log.Error(
|
||||||
|
$"Access violation during attempt to resolve material path\nDraw object: {(nint)CharacterBase:X} (of type {ModelType})\nSlot index: {SlotIndex}\nMaterial file name: {(nint)mtrlFileName:X} ({new string((sbyte*)mtrlFileName)})");
|
||||||
return Utf8GamePath.Empty;
|
return Utf8GamePath.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,30 +241,23 @@ internal partial record ResolveContext
|
||||||
var characterRaceCode = (GenderRace)human->RaceSexId;
|
var characterRaceCode = (GenderRace)human->RaceSexId;
|
||||||
switch (partialSkeletonIndex)
|
switch (partialSkeletonIndex)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0: return (characterRaceCode, "base", 1);
|
||||||
return (characterRaceCode, "base", 1);
|
|
||||||
case 1:
|
case 1:
|
||||||
var faceId = human->FaceId;
|
var faceId = human->FaceId;
|
||||||
var tribe = human->Customize[(int)Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex.Tribe];
|
var tribe = human->Customize[(int)Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex.Tribe];
|
||||||
var modelType = human->Customize[(int)Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex.ModelType];
|
var modelType = human->Customize[(int)Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex.ModelType];
|
||||||
if (faceId < 201)
|
if (faceId < 201)
|
||||||
{
|
|
||||||
faceId -= tribe switch
|
faceId -= tribe switch
|
||||||
{
|
{
|
||||||
0xB when modelType == 4 => 100,
|
0xB when modelType == 4 => 100,
|
||||||
0xE | 0xF => 100,
|
0xE | 0xF => 100,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
return ResolveHumanExtraSkeletonData(characterRaceCode, EstType.Face, faceId);
|
return ResolveHumanExtraSkeletonData(characterRaceCode, EstType.Face, faceId);
|
||||||
case 2:
|
case 2: return ResolveHumanExtraSkeletonData(characterRaceCode, EstType.Hair, human->HairId);
|
||||||
return ResolveHumanExtraSkeletonData(characterRaceCode, EstType.Hair, human->HairId);
|
case 3: return ResolveHumanEquipmentSkeletonData(EquipSlot.Head, EstType.Head);
|
||||||
case 3:
|
case 4: return ResolveHumanEquipmentSkeletonData(EquipSlot.Body, EstType.Body);
|
||||||
return ResolveHumanEquipmentSkeletonData(EquipSlot.Head, EstType.Head);
|
default: return (0, string.Empty, 0);
|
||||||
case 4:
|
|
||||||
return ResolveHumanEquipmentSkeletonData(EquipSlot.Body, EstType.Body);
|
|
||||||
default:
|
|
||||||
return (0, string.Empty, 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -269,7 +268,8 @@ internal partial record ResolveContext
|
||||||
return ResolveHumanExtraSkeletonData(ResolveEqdpRaceCode(slot, equipment.Set), type, equipment.Set);
|
return ResolveHumanExtraSkeletonData(ResolveEqdpRaceCode(slot, equipment.Set), type, equipment.Set);
|
||||||
}
|
}
|
||||||
|
|
||||||
private (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstType type, PrimaryId primary)
|
private (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstType type,
|
||||||
|
PrimaryId primary)
|
||||||
{
|
{
|
||||||
var metaCache = Global.Collection.MetaCache;
|
var metaCache = Global.Collection.MetaCache;
|
||||||
var skeletonSet = metaCache?.GetEstEntry(type, raceCode, primary) ?? default;
|
var skeletonSet = metaCache?.GetEstEntry(type, raceCode, primary) ?? default;
|
||||||
|
|
|
||||||
|
|
@ -73,11 +73,11 @@ public class ResourceTree
|
||||||
|
|
||||||
var genericContext = globalContext.CreateContext(model);
|
var genericContext = globalContext.CreateContext(model);
|
||||||
|
|
||||||
for (var i = 0; i < model->SlotCount; ++i)
|
for (var i = 0u; i < model->SlotCount; ++i)
|
||||||
{
|
{
|
||||||
var slotContext = i < equipment.Length
|
var slotContext = i < equipment.Length
|
||||||
? globalContext.CreateContext(model, (uint)i, ((uint)i).ToEquipSlot(), equipment[i])
|
? globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i])
|
||||||
: globalContext.CreateContext(model, (uint)i);
|
: globalContext.CreateContext(model, i);
|
||||||
|
|
||||||
var imc = (ResourceHandle*)model->IMCArray[i];
|
var imc = (ResourceHandle*)model->IMCArray[i];
|
||||||
var imcNode = slotContext.CreateNodeFromImc(imc);
|
var imcNode = slotContext.CreateNodeFromImc(imc);
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,11 @@ using OtterGui.Classes;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.Import;
|
using Penumbra.Import;
|
||||||
using Penumbra.Mods.Editor;
|
using Penumbra.Mods.Editor;
|
||||||
|
using Penumbra.Services;
|
||||||
|
|
||||||
namespace Penumbra.Mods.Manager;
|
namespace Penumbra.Mods.Manager;
|
||||||
|
|
||||||
public class ModImportManager(ModManager modManager, Configuration config, ModEditor modEditor) : IDisposable, IService
|
public class ModImportManager(ModManager modManager, Configuration config, ModEditor modEditor, MigrationManager migrationManager) : IDisposable, IService
|
||||||
{
|
{
|
||||||
private readonly ConcurrentQueue<string[]> _modsToUnpack = new();
|
private readonly ConcurrentQueue<string[]> _modsToUnpack = new();
|
||||||
|
|
||||||
|
|
@ -42,7 +43,7 @@ public class ModImportManager(ModManager modManager, Configuration config, ModEd
|
||||||
if (files.Length == 0)
|
if (files.Length == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_import = new TexToolsImporter(files.Length, files, AddNewMod, config, modEditor, modManager, modEditor.Compactor);
|
_import = new TexToolsImporter(files.Length, files, AddNewMod, config, modEditor, modManager, modEditor.Compactor, migrationManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Importing
|
public bool Importing
|
||||||
|
|
|
||||||
287
Penumbra/Services/MigrationManager.cs
Normal file
287
Penumbra/Services/MigrationManager.cs
Normal file
|
|
@ -0,0 +1,287 @@
|
||||||
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData.Files;
|
||||||
|
using SharpCompress.Common;
|
||||||
|
using SharpCompress.Readers;
|
||||||
|
|
||||||
|
namespace Penumbra.Services;
|
||||||
|
|
||||||
|
public class MigrationManager(Configuration config) : IService
|
||||||
|
{
|
||||||
|
private Task? _currentTask;
|
||||||
|
private CancellationTokenSource? _source;
|
||||||
|
|
||||||
|
public bool HasCleanUpTask { get; private set; }
|
||||||
|
public bool HasMigrationTask { get; private set; }
|
||||||
|
public bool HasRestoreTask { get; private set; }
|
||||||
|
|
||||||
|
public bool IsMigrationTask { get; private set; }
|
||||||
|
public bool IsRestorationTask { get; private set; }
|
||||||
|
public bool IsCleanupTask { get; private set; }
|
||||||
|
|
||||||
|
|
||||||
|
public int Restored { get; private set; }
|
||||||
|
public int RestoreFails { get; private set; }
|
||||||
|
|
||||||
|
public int CleanedUp { get; private set; }
|
||||||
|
|
||||||
|
public int CleanupFails { get; private set; }
|
||||||
|
|
||||||
|
public int Migrated { get; private set; }
|
||||||
|
|
||||||
|
public int Unchanged { get; private set; }
|
||||||
|
|
||||||
|
public int Failed { get; private set; }
|
||||||
|
|
||||||
|
public bool IsRunning
|
||||||
|
=> _currentTask is { IsCompleted: false };
|
||||||
|
|
||||||
|
/// <summary> Writes or migrates a .mdl file during extraction from a regular archive. </summary>
|
||||||
|
public void MigrateMdlDuringExtraction(IReader reader, string directory, ExtractionOptions options)
|
||||||
|
{
|
||||||
|
if (!config.MigrateImportedModelsToV6)
|
||||||
|
{
|
||||||
|
reader.WriteEntryToDirectory(directory, options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = Path.Combine(directory, reader.Entry.Key);
|
||||||
|
using var s = new MemoryStream();
|
||||||
|
using var e = reader.OpenEntryStream();
|
||||||
|
e.CopyTo(s);
|
||||||
|
using var b = new BinaryReader(s);
|
||||||
|
var version = b.ReadUInt32();
|
||||||
|
if (version == MdlFile.V5)
|
||||||
|
{
|
||||||
|
var data = s.ToArray();
|
||||||
|
var mdl = new MdlFile(data);
|
||||||
|
MigrateModel(path, mdl, false);
|
||||||
|
Penumbra.Log.Debug($"Migrated model {reader.Entry.Key} from V5 to V6 during import.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
using var f = File.Open(path, FileMode.Create, FileAccess.Write);
|
||||||
|
s.Seek(0, SeekOrigin.Begin);
|
||||||
|
s.WriteTo(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CleanBackups(string path)
|
||||||
|
{
|
||||||
|
if (IsRunning)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_source = new CancellationTokenSource();
|
||||||
|
var token = _source.Token;
|
||||||
|
_currentTask = Task.Run(() =>
|
||||||
|
{
|
||||||
|
HasCleanUpTask = true;
|
||||||
|
IsCleanupTask = true;
|
||||||
|
IsMigrationTask = false;
|
||||||
|
IsRestorationTask = false;
|
||||||
|
CleanedUp = 0;
|
||||||
|
CleanupFails = 0;
|
||||||
|
foreach (var file in Directory.EnumerateFiles(path, "*.mdl.bak", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(file);
|
||||||
|
++CleanedUp;
|
||||||
|
Penumbra.Log.Debug($"Deleted model backup file {file}.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Penumbra.Messager.NotificationMessage(ex, $"Failed to delete model backup file {file}", NotificationType.Warning);
|
||||||
|
++CleanupFails;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RestoreBackups(string path)
|
||||||
|
{
|
||||||
|
if (IsRunning)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_source = new CancellationTokenSource();
|
||||||
|
var token = _source.Token;
|
||||||
|
_currentTask = Task.Run(() =>
|
||||||
|
{
|
||||||
|
HasRestoreTask = true;
|
||||||
|
IsCleanupTask = false;
|
||||||
|
IsMigrationTask = false;
|
||||||
|
IsRestorationTask = true;
|
||||||
|
CleanedUp = 0;
|
||||||
|
CleanupFails = 0;
|
||||||
|
foreach (var file in Directory.EnumerateFiles(path, "*.mdl.bak", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var target = file[..^4];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Copy(file, target, true);
|
||||||
|
++Restored;
|
||||||
|
Penumbra.Log.Debug($"Restored model backup file {file} to {target}.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Penumbra.Messager.NotificationMessage(ex, $"Failed to restore model backup file {file} to {target}",
|
||||||
|
NotificationType.Warning);
|
||||||
|
++RestoreFails;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Update the data of a .mdl file during TTMP extraction. Returns either the existing array or a new one. </summary>
|
||||||
|
public byte[] MigrateTtmpModel(string path, byte[] data)
|
||||||
|
{
|
||||||
|
FixLodNum(data);
|
||||||
|
if (!config.MigrateImportedModelsToV6)
|
||||||
|
return data;
|
||||||
|
|
||||||
|
var version = BitConverter.ToUInt32(data);
|
||||||
|
if (version != 5)
|
||||||
|
return data;
|
||||||
|
|
||||||
|
var mdl = new MdlFile(data);
|
||||||
|
if (!mdl.ConvertV5ToV6())
|
||||||
|
return data;
|
||||||
|
|
||||||
|
data = mdl.Write();
|
||||||
|
Penumbra.Log.Debug($"Migrated model {path} from V5 to V6 during import.");
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MigrateDirectory(string path, bool createBackups)
|
||||||
|
{
|
||||||
|
if (IsRunning)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_source = new CancellationTokenSource();
|
||||||
|
var token = _source.Token;
|
||||||
|
_currentTask = Task.Run(() =>
|
||||||
|
{
|
||||||
|
HasMigrationTask = true;
|
||||||
|
IsCleanupTask = false;
|
||||||
|
IsMigrationTask = true;
|
||||||
|
IsRestorationTask = false;
|
||||||
|
Unchanged = 0;
|
||||||
|
Migrated = 0;
|
||||||
|
Failed = 0;
|
||||||
|
foreach (var file in Directory.EnumerateFiles(path, "*.mdl", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var timer = Stopwatch.StartNew();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = File.ReadAllBytes(file);
|
||||||
|
var mdl = new MdlFile(data);
|
||||||
|
if (MigrateModel(file, mdl, createBackups))
|
||||||
|
{
|
||||||
|
++Migrated;
|
||||||
|
Penumbra.Log.Debug($"Migrated model file {file} from V5 to V6 in {timer.ElapsedMilliseconds} ms.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
++Unchanged;
|
||||||
|
Penumbra.Log.Verbose($"Verified that model file {file} is already V6 in {timer.ElapsedMilliseconds} ms.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Penumbra.Messager.NotificationMessage(ex, $"Failed to migrate model file {file} to V6 in {timer.ElapsedMilliseconds} ms",
|
||||||
|
NotificationType.Warning);
|
||||||
|
++Failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
_source?.Cancel();
|
||||||
|
_source = null;
|
||||||
|
_currentTask = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void FixLodNum(byte[] data)
|
||||||
|
{
|
||||||
|
const int modelHeaderLodOffset = 22;
|
||||||
|
|
||||||
|
// Model file header LOD num
|
||||||
|
data[64] = 1;
|
||||||
|
|
||||||
|
// Model header LOD num
|
||||||
|
var stackSize = BitConverter.ToUInt32(data, 4);
|
||||||
|
var runtimeBegin = stackSize + 0x44;
|
||||||
|
var stringsLengthOffset = runtimeBegin + 4;
|
||||||
|
var stringsLength = BitConverter.ToUInt32(data, (int)stringsLengthOffset);
|
||||||
|
var modelHeaderStart = stringsLengthOffset + stringsLength + 4;
|
||||||
|
data[modelHeaderStart + modelHeaderLodOffset] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryMigrateSingleModel(string path, bool createBackup)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = File.ReadAllBytes(path);
|
||||||
|
var mdl = new MdlFile(data);
|
||||||
|
return MigrateModel(path, mdl, createBackup);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Penumbra.Messager.NotificationMessage(ex, $"Failed to migrate the model {path} to V6", NotificationType.Warning);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryMigrateSingleMaterial(string path, bool createBackup)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = File.ReadAllBytes(path);
|
||||||
|
var mtrl = new MtrlFile(data);
|
||||||
|
return MigrateMaterial(path, mtrl, createBackup);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Penumbra.Messager.NotificationMessage(ex, $"Failed to migrate the material {path} to Dawntrail", NotificationType.Warning);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MigrateModel(string path, MdlFile mdl, bool createBackup)
|
||||||
|
{
|
||||||
|
if (!mdl.ConvertV5ToV6())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var data = mdl.Write();
|
||||||
|
if (createBackup)
|
||||||
|
File.Copy(path, Path.ChangeExtension(path, ".mdl.bak"));
|
||||||
|
File.WriteAllBytes(path, data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MigrateMaterial(string path, MtrlFile mtrl, bool createBackup)
|
||||||
|
{
|
||||||
|
if (!mtrl.MigrateToDawntrail())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var data = mtrl.Write();
|
||||||
|
|
||||||
|
mtrl.Write();
|
||||||
|
if (createBackup)
|
||||||
|
File.Copy(path, Path.ChangeExtension(path, ".mtrl.bak"));
|
||||||
|
File.WriteAllBytes(path, data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ using Dalamud.Interface.Utility;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
|
using OtterGui.Text;
|
||||||
using OtterGui.Widgets;
|
using OtterGui.Widgets;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
|
|
@ -16,6 +17,7 @@ public partial class ModEditWindow
|
||||||
|
|
||||||
private bool DrawMaterialPanel(MtrlTab tab, bool disabled)
|
private bool DrawMaterialPanel(MtrlTab tab, bool disabled)
|
||||||
{
|
{
|
||||||
|
DrawVersionUpdate(tab, disabled);
|
||||||
DrawMaterialLivePreviewRebind(tab, disabled);
|
DrawMaterialLivePreviewRebind(tab, disabled);
|
||||||
|
|
||||||
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
|
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
|
||||||
|
|
@ -34,6 +36,20 @@ public partial class ModEditWindow
|
||||||
return !disabled && ret;
|
return !disabled && ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawVersionUpdate(MtrlTab tab, bool disabled)
|
||||||
|
{
|
||||||
|
if (disabled || tab.Mtrl.IsDawnTrail)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!ImUtf8.ButtonEx("Update MTRL Version to Dawntrail"u8,
|
||||||
|
"Try using this if the material can not be loaded or should use legacy shaders.\n\nThis is not revertible."u8,
|
||||||
|
new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg))
|
||||||
|
return;
|
||||||
|
|
||||||
|
tab.Mtrl.MigrateToDawntrail();
|
||||||
|
_materialTab.SaveFile();
|
||||||
|
}
|
||||||
|
|
||||||
private static void DrawMaterialLivePreviewRebind(MtrlTab tab, bool disabled)
|
private static void DrawMaterialLivePreviewRebind(MtrlTab tab, bool disabled)
|
||||||
{
|
{
|
||||||
if (disabled)
|
if (disabled)
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
||||||
{
|
{
|
||||||
private const string WindowBaseLabel = "###SubModEdit";
|
private const string WindowBaseLabel = "###SubModEdit";
|
||||||
|
|
||||||
|
public readonly MigrationManager MigrationManager;
|
||||||
|
|
||||||
private readonly PerformanceTracker _performance;
|
private readonly PerformanceTracker _performance;
|
||||||
private readonly ModEditor _editor;
|
private readonly ModEditor _editor;
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
|
|
@ -588,7 +590,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
||||||
StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab,
|
StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab,
|
||||||
CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager,
|
CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager,
|
||||||
ResourceTreeViewerFactory resourceTreeViewerFactory, ObjectManager objects, IFramework framework,
|
ResourceTreeViewerFactory resourceTreeViewerFactory, ObjectManager objects, IFramework framework,
|
||||||
CharacterBaseDestructor characterBaseDestructor, MetaDrawers metaDrawers)
|
CharacterBaseDestructor characterBaseDestructor, MetaDrawers metaDrawers, MigrationManager migrationManager)
|
||||||
: base(WindowBaseLabel)
|
: base(WindowBaseLabel)
|
||||||
{
|
{
|
||||||
_performance = performance;
|
_performance = performance;
|
||||||
|
|
@ -608,6 +610,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
||||||
_objects = objects;
|
_objects = objects;
|
||||||
_framework = framework;
|
_framework = framework;
|
||||||
_characterBaseDestructor = characterBaseDestructor;
|
_characterBaseDestructor = characterBaseDestructor;
|
||||||
|
MigrationManager = migrationManager;
|
||||||
_metaDrawers = metaDrawers;
|
_metaDrawers = metaDrawers;
|
||||||
_materialTab = new FileEditor<MtrlTab>(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl",
|
_materialTab = new FileEditor<MtrlTab>(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl",
|
||||||
() => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty,
|
() => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty,
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,9 @@ public class CollectionSelectHeader : IUiService
|
||||||
var buttonSize = new Vector2(comboWidth * 3f / 4f, 0f);
|
var buttonSize = new Vector2(comboWidth * 3f / 4f, 0f);
|
||||||
using (var _ = ImRaii.Group())
|
using (var _ = ImRaii.Group())
|
||||||
{
|
{
|
||||||
DrawCollectionButton(buttonSize, GetDefaultCollectionInfo(), 1);
|
DrawCollectionButton(buttonSize, GetDefaultCollectionInfo(), 1);
|
||||||
DrawCollectionButton(buttonSize, GetInterfaceCollectionInfo(), 2);
|
DrawCollectionButton(buttonSize, GetInterfaceCollectionInfo(), 2);
|
||||||
DrawCollectionButton(buttonSize, GetPlayerCollectionInfo(), 3);
|
DrawCollectionButton(buttonSize, GetPlayerCollectionInfo(), 3);
|
||||||
DrawCollectionButton(buttonSize, GetInheritedCollectionInfo(), 4);
|
DrawCollectionButton(buttonSize, GetInheritedCollectionInfo(), 4);
|
||||||
|
|
||||||
_collectionCombo.Draw("##collectionSelector", comboWidth, ColorId.SelectedCollection.Value());
|
_collectionCombo.Draw("##collectionSelector", comboWidth, ColorId.SelectedCollection.Value());
|
||||||
|
|
|
||||||
121
Penumbra/UI/Classes/MigrationSectionDrawer.cs
Normal file
121
Penumbra/UI/Classes/MigrationSectionDrawer.cs
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
using ImGuiNET;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using OtterGui.Text;
|
||||||
|
using Penumbra.Services;
|
||||||
|
|
||||||
|
namespace Penumbra.UI.Classes;
|
||||||
|
|
||||||
|
public class MigrationSectionDrawer(MigrationManager migrationManager, Configuration config) : IUiService
|
||||||
|
{
|
||||||
|
private bool _createBackups = true;
|
||||||
|
private Vector2 _buttonSize;
|
||||||
|
|
||||||
|
public void Draw()
|
||||||
|
{
|
||||||
|
using var header = ImUtf8.CollapsingHeaderId("Migration"u8);
|
||||||
|
if (!header)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_buttonSize = UiHelpers.InputTextWidth;
|
||||||
|
DrawSettings();
|
||||||
|
ImGui.Separator();
|
||||||
|
DrawMigration();
|
||||||
|
ImGui.Separator();
|
||||||
|
DrawCleanup();
|
||||||
|
ImGui.Separator();
|
||||||
|
DrawRestore();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawSettings()
|
||||||
|
{
|
||||||
|
var value = config.MigrateImportedModelsToV6;
|
||||||
|
if (ImUtf8.Checkbox("Automatically Migrate V5 Models to V6 on Import"u8, ref value))
|
||||||
|
{
|
||||||
|
config.MigrateImportedModelsToV6 = value;
|
||||||
|
config.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawMigration()
|
||||||
|
{
|
||||||
|
ImUtf8.Checkbox("Create Backups During Manual Migration", ref _createBackups);
|
||||||
|
if (ImUtf8.ButtonEx("Migrate Model Files From V5 to V6"u8, "\0"u8, _buttonSize, migrationManager.IsRunning))
|
||||||
|
migrationManager.MigrateDirectory(config.ModDirectory, _createBackups);
|
||||||
|
|
||||||
|
ImUtf8.SameLineInner();
|
||||||
|
DrawCancelButton(0, "Cancel the migration. This does not revert already finished migrations."u8);
|
||||||
|
DrawSpinner(migrationManager is { IsMigrationTask: true, IsRunning: true });
|
||||||
|
|
||||||
|
if (!migrationManager.HasMigrationTask)
|
||||||
|
{
|
||||||
|
ImUtf8.IconDummy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = migrationManager.Failed + migrationManager.Migrated + migrationManager.Unchanged;
|
||||||
|
if (total == 0)
|
||||||
|
ImUtf8.TextFrameAligned("No model files found."u8);
|
||||||
|
else
|
||||||
|
ImUtf8.TextFrameAligned($"{migrationManager.Migrated} files migrated, {migrationManager.Failed} files failed, {total} total files.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCleanup()
|
||||||
|
{
|
||||||
|
if (ImUtf8.ButtonEx("Delete Existing Model Backup Files"u8, "\0"u8, _buttonSize, migrationManager.IsRunning))
|
||||||
|
migrationManager.CleanBackups(config.ModDirectory);
|
||||||
|
|
||||||
|
ImUtf8.SameLineInner();
|
||||||
|
DrawCancelButton(1, "Cancel the cleanup. This is not revertible."u8);
|
||||||
|
DrawSpinner(migrationManager is { IsCleanupTask: true, IsRunning: true });
|
||||||
|
if (!migrationManager.HasCleanUpTask)
|
||||||
|
{
|
||||||
|
ImUtf8.IconDummy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = migrationManager.CleanedUp + migrationManager.CleanupFails;
|
||||||
|
if (total == 0)
|
||||||
|
ImUtf8.TextFrameAligned("No model backup files found."u8);
|
||||||
|
else
|
||||||
|
ImUtf8.TextFrameAligned(
|
||||||
|
$"{migrationManager.CleanedUp} backups deleted, {migrationManager.CleanupFails} deletions failed, {total} total backups.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawSpinner(bool enabled)
|
||||||
|
{
|
||||||
|
if (!enabled)
|
||||||
|
return;
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImUtf8.Spinner("Spinner"u8, ImGui.GetTextLineHeight() / 2, 2, ImGui.GetColorU32(ImGuiCol.Text));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawRestore()
|
||||||
|
{
|
||||||
|
if (ImUtf8.ButtonEx("Restore Model Backups"u8, "\0"u8, _buttonSize, migrationManager.IsRunning))
|
||||||
|
migrationManager.RestoreBackups(config.ModDirectory);
|
||||||
|
|
||||||
|
ImUtf8.SameLineInner();
|
||||||
|
DrawCancelButton(2, "Cancel the restoration. This does not revert already finished restoration."u8);
|
||||||
|
DrawSpinner(migrationManager is { IsRestorationTask: true, IsRunning: true });
|
||||||
|
|
||||||
|
if (!migrationManager.HasRestoreTask)
|
||||||
|
{
|
||||||
|
ImUtf8.IconDummy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = migrationManager.Restored + migrationManager.RestoreFails;
|
||||||
|
if (total == 0)
|
||||||
|
ImUtf8.TextFrameAligned("No model backup files found."u8);
|
||||||
|
else
|
||||||
|
ImUtf8.TextFrameAligned(
|
||||||
|
$"{migrationManager.Restored} backups restored, {migrationManager.RestoreFails} restorations failed, {total} total backups.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCancelButton(int id, ReadOnlySpan<byte> tooltip)
|
||||||
|
{
|
||||||
|
using var _ = ImUtf8.PushId(id);
|
||||||
|
if (ImUtf8.ButtonEx("Cancel"u8, tooltip, disabled: !migrationManager.IsRunning))
|
||||||
|
migrationManager.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -41,10 +41,11 @@ public class SettingsTab : ITab, IUiService
|
||||||
private readonly DalamudSubstitutionProvider _dalamudSubstitutionProvider;
|
private readonly DalamudSubstitutionProvider _dalamudSubstitutionProvider;
|
||||||
private readonly FileCompactor _compactor;
|
private readonly FileCompactor _compactor;
|
||||||
private readonly DalamudConfigService _dalamudConfig;
|
private readonly DalamudConfigService _dalamudConfig;
|
||||||
private readonly IDalamudPluginInterface _pluginInterface;
|
private readonly IDalamudPluginInterface _pluginInterface;
|
||||||
private readonly IDataManager _gameData;
|
private readonly IDataManager _gameData;
|
||||||
private readonly PredefinedTagManager _predefinedTagManager;
|
private readonly PredefinedTagManager _predefinedTagManager;
|
||||||
private readonly CrashHandlerService _crashService;
|
private readonly CrashHandlerService _crashService;
|
||||||
|
private readonly MigrationSectionDrawer _migrationDrawer;
|
||||||
|
|
||||||
private int _minimumX = int.MaxValue;
|
private int _minimumX = int.MaxValue;
|
||||||
private int _minimumY = int.MaxValue;
|
private int _minimumY = int.MaxValue;
|
||||||
|
|
@ -55,7 +56,8 @@ public class SettingsTab : ITab, IUiService
|
||||||
Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector,
|
Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector,
|
||||||
CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi,
|
CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi,
|
||||||
DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig,
|
DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig,
|
||||||
IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService)
|
IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService,
|
||||||
|
MigrationSectionDrawer migrationDrawer)
|
||||||
{
|
{
|
||||||
_pluginInterface = pluginInterface;
|
_pluginInterface = pluginInterface;
|
||||||
_config = config;
|
_config = config;
|
||||||
|
|
@ -77,6 +79,7 @@ public class SettingsTab : ITab, IUiService
|
||||||
_compactor.Enabled = _config.UseFileSystemCompression;
|
_compactor.Enabled = _config.UseFileSystemCompression;
|
||||||
_predefinedTagManager = predefinedTagConfig;
|
_predefinedTagManager = predefinedTagConfig;
|
||||||
_crashService = crashService;
|
_crashService = crashService;
|
||||||
|
_migrationDrawer = migrationDrawer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DrawHeader()
|
public void DrawHeader()
|
||||||
|
|
@ -102,6 +105,7 @@ public class SettingsTab : ITab, IUiService
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
|
|
||||||
DrawGeneralSettings();
|
DrawGeneralSettings();
|
||||||
|
_migrationDrawer.Draw();
|
||||||
DrawColorSettings();
|
DrawColorSettings();
|
||||||
DrawPredefinedTagsSection();
|
DrawPredefinedTagsSection();
|
||||||
DrawAdvancedSettings();
|
DrawAdvancedSettings();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue