Add some migration things.

This commit is contained in:
Ottermandias 2024-07-08 14:55:49 +02:00
parent 0d939b12f4
commit 56e284a99e
17 changed files with 515 additions and 80 deletions

@ -1 +1 @@
Subproject commit c2738e1d42974cddbe5a31238c6ed236a831d17d Subproject commit 89b3b9513f9b4989045517a452ef971e24377203

@ -1 +1 @@
Subproject commit 62f6acfb0f9e31b2907c02a270d723bfff18b390 Subproject commit b5eb074d80a4aaa2703a3124d2973a7fa08046ad

View file

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

View file

@ -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(_ =>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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;
}
}

View file

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

View file

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

View file

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

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

View file

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