/// <summary>
        /// Performs post-patch modlist corrections and validation, prompting user also to generate backups after a successful completion.
        /// </summary>
        /// <returns></returns>
        public async Task DoPostPatchCleanup()
        {
            FlexibleMessageBox.Show(_mainWindow.Win32Window, UIMessages.PatchDetectedMessage, "Post Patch Cleanup Starting", MessageBoxButtons.OK, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1);
            MainWindow.MakeHighlander();

            var resetLumina = false;

            await _mainWindow.LockUi("Performing Post-Patch Maintenence", "This may take a few minutes if you have many mods installed.", this);

            var gi = XivCache.GameInfo;

            if (XivCache.GameInfo.UseLumina)
            {
                resetLumina = true;
                XivCache.SetGameInfo(gi.GameDirectory, gi.GameLanguage, gi.DxMode, false, false, gi.LuminaDirectory, gi.UseLumina);
            }

            var workerStatus = XivCache.CacheWorkerEnabled;

            if (workerStatus)
            {
                // Stop the cache worker if it's running.
                XivCache.CacheWorkerEnabled = false;
            }
            try
            {
                var modding = new Modding(_gameDirectory);
                var _index  = new Index(_gameDirectory);
                var _dat    = new Dat(_gameDirectory);

                var validTypes = new List <int>()
                {
                    2, 3, 4
                };

                // We have to do a few things here.
                // 1.  Save a list of what mods were enabled.
                // 2.  Go through and validate everything that says it is enabled actually is enabled, or mark it as disabled and update its original index offset if it is not.
                // 3.  Prompt the user for either a full disable and backup creation, or a restore to normal state (re-enable anything that was enabled before but is not now)
                var modList = modding.GetModList();

                Dictionary <XivDataFile, IndexFile> indexFiles = new Dictionary <XivDataFile, IndexFile>();


                // Cache our currently enabled stuff.
                List <Mod> enabledMods = modList.Mods.Where(x => x.enabled == true).ToList();
                var        toRemove    = new List <Mod>();

                foreach (var mod in modList.Mods)
                {
                    if (!String.IsNullOrEmpty(mod.fullPath))
                    {
                        var df = IOUtil.GetDataFileFromPath(mod.fullPath);
                        if (!indexFiles.ContainsKey(df))
                        {
                            indexFiles[df] = await _index.GetIndexFile(df);
                        }

                        var index1Value       = indexFiles[df].Get8xDataOffset(mod.fullPath);
                        var index2Value       = indexFiles[df].Get8xDataOffsetIndex2(mod.fullPath);
                        var oldOriginalOffset = mod.data.originalOffset;
                        var modOffset         = mod.data.modOffset;


                        // In any event where an offset does not match either of our saved offsets, we must assume this is a new
                        // default file offset for post-patch.
                        if (index1Value != oldOriginalOffset && index1Value != modOffset && index1Value != 0)
                        {
                            // Index 1 value is our new base offset.
                            var type = _dat.GetFileType(index1Value, df);

                            // Make sure the file it's trying to point to is actually valid.
                            if (validTypes.Contains(type))
                            {
                                mod.data.originalOffset = index1Value;
                                mod.enabled             = false;
                            }
                            else
                            {
                                // Oh dear.  The new index is f****d.  Is the old Index Ok?
                                type = _dat.GetFileType(oldOriginalOffset, df);

                                if (validTypes.Contains(type) && oldOriginalOffset != 0)
                                {
                                    // Old index is fine, so keep using that.

                                    // But mark the index value as invalid, so that we stomp on the index value after this.
                                    index1Value = -1;
                                    mod.enabled = false;
                                }
                                else
                                {
                                    // Okay... Maybe the new Index2 Value?
                                    if (index2Value != 0)
                                    {
                                        type = _dat.GetFileType(index2Value, df);
                                        if (validTypes.Contains(type))
                                        {
                                            // Set the index 1 value to invalid so that the if later down the chain stomps the index1 value.
                                            index1Value = -1;

                                            mod.data.originalOffset = index2Value;
                                            mod.enabled             = false;
                                        }
                                        else
                                        {
                                            // We be f****d.
                                            throw new Exception("Unable to determine working original offset for file:" + mod.fullPath);
                                        }
                                    }
                                    else
                                    {
                                        // We be f****d.
                                        throw new Exception("Unable to determine working original offset for file:" + mod.fullPath);
                                    }
                                }
                            }
                        }
                        else if (index2Value != oldOriginalOffset && index2Value != modOffset && index2Value != 0)
                        {
                            // Our Index 1 was normal, but our Index 2 is changed to an unknown value.
                            // If the index 2 points to a valid file, we must assume that this new file
                            // is our new base data offset.

                            var type = _dat.GetFileType(index2Value, df);

                            if (validTypes.Contains(type) && index2Value != 0)
                            {
                                mod.data.originalOffset = index2Value;
                                mod.enabled             = false;
                            }
                            else
                            {
                                // Oh dear.  The new index is f****d.  Is the old Index Ok?
                                type = _dat.GetFileType(oldOriginalOffset, df);

                                if (validTypes.Contains(type) && oldOriginalOffset != 0)
                                {
                                    // Old index is fine, so keep using that, but set the index2 value to invalid to ensure we
                                    // stomp on the current broken index value.
                                    index2Value = -1;
                                }
                                else
                                {
                                    // We be f****d.
                                    throw new Exception("Unable to determine working original offset for file:" + mod.fullPath);
                                }
                            }
                        }

                        // Indexes don't match.  This can occur if SE adds something to index2 that didn't exist in index2 before.
                        if (index1Value != index2Value && index2Value != 0)
                        {
                            // We should never actually get to this state for file-addition mods.  If we do, uh.. I guess correct the indexes and yolo?
                            // ( Only way we get here is if SE added a new file at the same name as a file the user had created via modding, in which case, it's technically no longer a file addition mod )
                            indexFiles[df].SetDataOffset(mod.fullPath, mod.data.originalOffset);
                            index1Value = mod.data.originalOffset;
                            index2Value = mod.data.originalOffset;

                            mod.enabled = false;
                        }

                        // Set it to the corrected state.
                        if (index1Value == mod.data.modOffset)
                        {
                            mod.enabled = true;
                        }
                        else
                        {
                            mod.enabled = false;
                        }

                        // Perform a basic file type check on our results.
                        var fileType         = _dat.GetFileType(mod.data.modOffset, IOUtil.GetDataFileFromPath(mod.fullPath));
                        var originalFileType = _dat.GetFileType(mod.data.modOffset, IOUtil.GetDataFileFromPath(mod.fullPath));

                        if (!validTypes.Contains(fileType) || mod.data.modOffset == 0)
                        {
                            // Mod data is busted.  Fun.
                            toRemove.Add(mod);
                        }

                        if ((!validTypes.Contains(originalFileType)) || mod.data.originalOffset == 0)
                        {
                            if (mod.IsCustomFile())
                            {
                                // Okay, in this case this is recoverable as the mod is a custom addition anyways, so we can just delete it.
                                if (!toRemove.Contains(mod))
                                {
                                    toRemove.Add(mod);
                                }
                            }
                            else
                            {
                                // Update ended up with us unable to find a working offset.  Double fun.
                                throw new Exception("Unable to determine working offset for file:" + mod.fullPath);
                            }
                        }
                    }

                    // Okay, this mod is now represented in the modlist in it's actual post-patch index state.
                    var datNum = (int)((mod.data.modOffset / 8) & 0x0F) / 2;
                    var dat    = XivDataFiles.GetXivDataFile(mod.datFile);

                    var originalDats = await _dat.GetUnmoddedDatList(dat);

                    var datPath = $"{dat.GetDataFileName()}{Dat.DatExtension}{datNum}";

                    // Test for SE Dat file rollover.
                    if (originalDats.Contains(datPath))
                    {
                        // Shit.  This means that the dat file where this mod lived got eaten by SE.  We have to destroy the modlist entry at this point.
                        toRemove.Add(mod);
                    }
                }

                // Save any index changes we made.
                foreach (var dkv in indexFiles)
                {
                    await _index.SaveIndexFile(dkv.Value);
                }

                // The modlist is now saved in its current index-represented post patch state.
                modding.SaveModList(modList);

                // We now need to clear out any mods that are irreparably f****d, and clear out all of our
                // internal data files so we can rebuild them later.

                var internalFiles = modList.Mods.Where(x => x.IsInternal());
                toRemove.AddRange(internalFiles);

                if (toRemove.Count > 0)
                {
                    var removedString = "";

                    // Soft-Disable all metadata mods, since we're going to purge their internal file entries.
                    var metadata = modList.Mods.Where(x => x.fullPath.EndsWith(".meta") || x.fullPath.EndsWith(".rgsp"));
                    foreach (var mod in metadata)
                    {
                        var df = IOUtil.GetDataFileFromPath(mod.fullPath);
                        await modding.ToggleModUnsafe(false, mod, true, false, indexFiles[df], modList);
                    }

                    foreach (var mod in toRemove)
                    {
                        if (mod.data.modOffset == 0 || mod.data.originalOffset == 0)
                        {
                            if (mod.data.originalOffset == 0 && mod.enabled)
                            {
                                // This is awkward.  We have a mod whose data got bashed, but has no valid original offset to restore.
                                // So the indexes here are f****d if we do, f****d if we don't.
                                throw new Exception("Patch-Broken file has no valid index to restore.  Clean Index Restoration required.");
                            }

                            modList.Mods.Remove(mod);
                            enabledMods.Remove(mod);
                            removedString += mod.fullPath + "\n";
                        }
                        else
                        {
                            if (mod.enabled)
                            {
                                var df = IOUtil.GetDataFileFromPath(mod.fullPath);
                                await modding.ToggleModUnsafe(false, mod, true, false, indexFiles[df], modList);
                            }

                            modList.Mods.Remove(mod);

                            // Since we're deleting this entry entirely, we can't leave it in the other cached list either to get re-enabled later.
                            enabledMods.Remove(mod);

                            if (!mod.IsInternal())
                            {
                                removedString += mod.fullPath + "\n";
                            }
                        }
                    }

                    // Save the index files and modlist again now that we've removed all the invalid entries.
                    foreach (var dkv in indexFiles)
                    {
                        await _index.SaveIndexFile(dkv.Value);
                    }
                    modding.SaveModList(modList);

                    // Show the user a message if we purged any real files.
                    if (toRemove.Any(x => !String.IsNullOrEmpty(x.fullPath) && !x.IsInternal()))
                    {
                        var text = String.Format(UIMessages.PatchDestroyedFiles, removedString);

                        FlexibleMessageBox.Show(_mainWindow.Win32Window, text, "Destroyed Files Notification", MessageBoxButtons.OK, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button1);
                    }
                }

                // Always create clean index backups after this process is completed.

                _mainWindow.LockProgress.Report("Disabling Mods...");
                await modding.ToggleAllMods(false);

                _mainWindow.LockProgress.Report("Creating Index Backups...");
                var           pc = new ProblemChecker(_gameDirectory);
                DirectoryInfo backupDir;
                try
                {
                    Directory.CreateDirectory(Settings.Default.Backup_Directory);
                    backupDir = new DirectoryInfo(Settings.Default.Backup_Directory);
                }
                catch
                {
                    throw new Exception("Unable to create index backups.\nThe Index Backup directory is invalid or inaccessible: " + Settings.Default.Backup_Directory);
                }

                await pc.BackupIndexFiles(backupDir);

                // Now restore the modlist enable/disable state back to how the user had it before.
                _mainWindow.LockProgress.Report("Re-Enabling mods...");

                // Re-enable things.
                await modding.ToggleMods(true, enabledMods.Select(x => x.fullPath));

                FlexibleMessageBox.Show(_mainWindow.Win32Window, UIMessages.PostPatchComplete, "Post-Patch Process Complete", MessageBoxButtons.OK, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1);
            }
            catch (Exception Ex)
            {
                // Show the user the error, then let them go about their business of fixing things.
                FlexibleMessageBox.Show(_mainWindow.Win32Window, String.Format(UIMessages.PostPatchError, Ex.Message), "Post-Patch Failure", MessageBoxButtons.OK, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1);
            }
            finally
            {
                if (resetLumina)
                {
                    // Reset lumina mode back to on if we disabled it to perform update checks.
                    XivCache.SetGameInfo(gi.GameDirectory, gi.GameLanguage, gi.DxMode, true, false, gi.LuminaDirectory, true);
                }

                XivCache.CacheWorkerEnabled = workerStatus;
                await _mainWindow.UnlockUi(this);
            }
        }
        /// <summary>
        /// Applies this Metadata object to the FFXIV file system.
        /// This should only called by Dat.WriteToDat() / RestoreDefaultMetadata()
        /// </summary>
        internal static async Task ApplyMetadata(ItemMetadata meta, IndexFile index = null, ModList modlist = null)
        {
            var _eqp     = new Eqp(XivCache.GameInfo.GameDirectory);
            var _modding = new Modding(XivCache.GameInfo.GameDirectory);
            var _index   = new Index(XivCache.GameInfo.GameDirectory);
            var df       = IOUtil.GetDataFileFromPath(meta.Root.Info.GetRootFile());

            var dummyItem = new XivGenericItemModel();

            dummyItem.Name = Constants.InternalModSourceName;
            dummyItem.SecondaryCategory = Constants.InternalModSourceName;


            // Beep boop
            bool doSave = false;

            if (index == null)
            {
                doSave = true;
                index  = await _index.GetIndexFile(df);

                modlist = await _modding.GetModListAsync();
            }


            if (meta.ImcEntries.Count > 0)
            {
                var _imc    = new Imc(XivCache.GameInfo.GameDirectory);
                var imcPath = meta.Root.GetRawImcFilePath();
                await _imc.SaveEntries(imcPath, meta.Root.Info.Slot, meta.ImcEntries, dummyItem, index, modlist);
            }

            // Applying EQP data via set 0 is not allowed, as it is a special set hard-coded to use Set 1's data.
            if (meta.EqpEntry != null && !(meta.Root.Info.PrimaryType == Items.Enums.XivItemType.equipment && meta.Root.Info.PrimaryId == 0))
            {
                await _eqp.SaveEqpEntry(meta.Root.Info.PrimaryId, meta.EqpEntry, dummyItem, index, modlist);
            }

            if (meta.EqdpEntries.Count > 0)
            {
                await _eqp.SaveEqdpEntries((uint)meta.Root.Info.PrimaryId, meta.Root.Info.Slot, meta.EqdpEntries, dummyItem, index, modlist);
            }

            if (meta.EstEntries.Count > 0)
            {
                var type    = Est.GetEstType(meta.Root);
                var entries = meta.EstEntries.Values.ToList();
                await Est.SaveExtraSkeletonEntries(type, entries, dummyItem, index, modlist);
            }

            if (meta.GmpEntry != null)
            {
                await _eqp.SaveGimmickParameter(meta.Root.Info.PrimaryId, meta.GmpEntry, dummyItem, index, modlist);
            }

            if (doSave && !XivCache.GameInfo.UseLumina)
            {
                await _index.SaveIndexFile(index);

                await _modding.SaveModListAsync(modlist);
            }
        }
Beispiel #3
0
        /// <summary>
        /// Copies the entirety of a given root to a new root.
        /// </summary>
        /// <param name="Source">Original Root to copy from.</param>
        /// <param name="Destination">Destination root to copy to.</param>
        /// <param name="ApplicationSource">Application to list as the source for the resulting mod entries.</param>
        /// <returns>Returns a Dictionary of all the file conversion</returns>
        public static async Task <Dictionary <string, string> > CloneRoot(XivDependencyRoot Source, XivDependencyRoot Destination, string ApplicationSource, int singleVariant = -1, string saveDirectory = null, IProgress <string> ProgressReporter = null, IndexFile index = null, ModList modlist = null, ModPack modPack = null)
        {
            if (!IsSupported(Source) || !IsSupported(Destination))
            {
                throw new InvalidDataException("Cannot clone unsupported root.");
            }

            if (XivCache.GameInfo.UseLumina)
            {
                // Using the root cloner with Lumina import mode enabled is unstable/not safe.
                // ( The state of the game files in the TT system may not match the state of the
                // Lumina mod setup, and the modlist/etc. can get bashed as this function directly
                // modifies the modlist during the cloning process. )
                throw new Exception("Item Conversion cannot be used with Lumina import mode.");
            }


            if (ProgressReporter != null)
            {
                ProgressReporter.Report("Stopping Cache Worker...");
            }
            var workerStatus = XivCache.CacheWorkerEnabled;

            XivCache.CacheWorkerEnabled = false;
            try
            {
                var df = IOUtil.GetDataFileFromPath(Source.ToString());

                var _imc     = new Imc(XivCache.GameInfo.GameDirectory);
                var _mdl     = new Mdl(XivCache.GameInfo.GameDirectory, df);
                var _dat     = new Dat(XivCache.GameInfo.GameDirectory);
                var _index   = new Index(XivCache.GameInfo.GameDirectory);
                var _mtrl    = new Mtrl(XivCache.GameInfo.GameDirectory);
                var _modding = new Modding(XivCache.GameInfo.GameDirectory);

                var doSave = false;
                if (index == null)
                {
                    doSave = true;
                    index  = await _index.GetIndexFile(df);

                    modlist = await _modding.GetModListAsync();
                }


                bool locked = _index.IsIndexLocked(df);
                if (locked)
                {
                    throw new Exception("Game files currently in use.");
                }


                if (ProgressReporter != null)
                {
                    ProgressReporter.Report("Analyzing items and variants...");
                }

                // First, try to get everything, to ensure it's all valid.
                ItemMetadata originalMetadata = await GetCachedMetadata(index, modlist, Source, df, _dat);


                var originalModelPaths = await Source.GetModelFiles(index, modlist);

                var originalMaterialPaths = await Source.GetMaterialFiles(-1, index, modlist);

                var originalTexturePaths = await Source.GetTextureFiles(-1, index, modlist);

                var originalVfxPaths = new HashSet <string>();
                if (Imc.UsesImc(Source))
                {
                    var avfxSets = originalMetadata.ImcEntries.Select(x => x.Vfx).Distinct();
                    foreach (var avfx in avfxSets)
                    {
                        var avfxStuff = await ATex.GetVfxPath(Source.Info, avfx);

                        if (String.IsNullOrEmpty(avfxStuff.Folder) || String.IsNullOrEmpty(avfxStuff.File))
                        {
                            continue;
                        }

                        var path = avfxStuff.Folder + "/" + avfxStuff.File;
                        if (index.FileExists(path))
                        {
                            originalVfxPaths.Add(path);
                        }
                    }
                }

                // Time to start editing things.

                // First, get a new, clean copy of the metadata, pointed at the new root.
                var newMetadata = await GetCachedMetadata(index, modlist, Source, df, _dat);

                newMetadata.Root = Destination.Info.ToFullRoot();
                ItemMetadata originalDestinationMetadata = null;
                try
                {
                    originalDestinationMetadata = await GetCachedMetadata(index, modlist, Destination, df, _dat);
                } catch
                {
                    originalDestinationMetadata = new ItemMetadata(Destination);
                }

                // Set 0 needs special handling.
                if (Source.Info.PrimaryType == XivItemType.equipment && Source.Info.PrimaryId == 0)
                {
                    var set1Root     = new XivDependencyRoot(Source.Info.PrimaryType, 1, null, null, Source.Info.Slot);
                    var set1Metadata = await GetCachedMetadata(index, modlist, set1Root, df, _dat);

                    newMetadata.EqpEntry = set1Metadata.EqpEntry;

                    if (Source.Info.Slot == "met")
                    {
                        newMetadata.GmpEntry = set1Metadata.GmpEntry;
                    }
                }
                else if (Destination.Info.PrimaryType == XivItemType.equipment && Destination.Info.PrimaryId == 0)
                {
                    newMetadata.EqpEntry = null;
                    newMetadata.GmpEntry = null;
                }


                // Now figure out the path names for all of our new paths.
                // These dictionarys map Old Path => New Path
                Dictionary <string, string> newModelPaths        = new Dictionary <string, string>();
                Dictionary <string, string> newMaterialPaths     = new Dictionary <string, string>();
                Dictionary <string, string> newMaterialFileNames = new Dictionary <string, string>();
                Dictionary <string, string> newTexturePaths      = new Dictionary <string, string>();
                Dictionary <string, string> newAvfxPaths         = new Dictionary <string, string>();

                if (ProgressReporter != null)
                {
                    ProgressReporter.Report("Calculating files to copy...");
                }

                // For each path, replace any instances of our primary and secondary types.
                foreach (var path in originalModelPaths)
                {
                    newModelPaths.Add(path, UpdatePath(Source, Destination, path));
                }

                foreach (var path in originalMaterialPaths)
                {
                    var nPath = UpdatePath(Source, Destination, path);
                    newMaterialPaths.Add(path, nPath);
                    var fName = Path.GetFileName(path);

                    if (!newMaterialFileNames.ContainsKey(fName))
                    {
                        newMaterialFileNames.Add(fName, Path.GetFileName(nPath));
                    }
                }

                foreach (var path in originalTexturePaths)
                {
                    newTexturePaths.Add(path, UpdatePath(Source, Destination, path));
                }

                foreach (var path in originalVfxPaths)
                {
                    newAvfxPaths.Add(path, UpdatePath(Source, Destination, path));
                }

                var destItem = Destination.GetFirstItem();
                var srcItem  = (await Source.GetAllItems(singleVariant))[0];
                var iCat     = destItem.SecondaryCategory;
                var iName    = destItem.Name;


                var files = newModelPaths.Select(x => x.Value).Union(
                    newMaterialPaths.Select(x => x.Value)).Union(
                    newAvfxPaths.Select(x => x.Value)).Union(
                    newTexturePaths.Select(x => x.Value));
                var allFiles = new HashSet <string>();
                foreach (var f in files)
                {
                    allFiles.Add(f);
                }

                allFiles.Add(Destination.Info.GetRootFile());

                if (ProgressReporter != null)
                {
                    ProgressReporter.Report("Getting modlist...");
                }

                if (ProgressReporter != null)
                {
                    ProgressReporter.Report("Removing existing modifications to destination root...");
                }

                if (Destination != Source)
                {
                    var dPath   = Destination.Info.GetRootFolder();
                    var allMods = modlist.Mods.ToList();
                    foreach (var mod in allMods)
                    {
                        if (mod.fullPath.StartsWith(dPath) && !mod.IsInternal())
                        {
                            if (Destination.Info.SecondaryType != null || Destination.Info.Slot == null)
                            {
                                // If this is a slotless root, purge everything.
                                await _modding.DeleteMod(mod.fullPath, false, index, modlist);
                            }
                            else if (allFiles.Contains(mod.fullPath) || mod.fullPath.Contains(Destination.Info.GetBaseFileName(true)))
                            {
                                // Otherwise, only purge the files we're replacing, and anything else that
                                // contains our slot name.
                                await _modding.DeleteMod(mod.fullPath, false, index, modlist);
                            }
                        }
                    }
                }

                if (ProgressReporter != null)
                {
                    ProgressReporter.Report("Copying models...");
                }

                // Load, Edit, and resave the model files.
                foreach (var kv in newModelPaths)
                {
                    var src    = kv.Key;
                    var dst    = kv.Value;
                    var offset = index.Get8xDataOffset(src);
                    var xmdl   = await _mdl.GetRawMdlData(src, false, offset);

                    var tmdl = TTModel.FromRaw(xmdl);

                    if (xmdl == null || tmdl == null)
                    {
                        continue;
                    }

                    tmdl.Source  = dst;
                    xmdl.MdlPath = dst;

                    // Replace any material references as needed.
                    foreach (var m in tmdl.MeshGroups)
                    {
                        foreach (var matKv in newMaterialFileNames)
                        {
                            m.Material = m.Material.Replace(matKv.Key, matKv.Value);
                        }
                    }

                    // Save new Model.
                    var bytes = await _mdl.MakeNewMdlFile(tmdl, xmdl, null);

                    await _dat.WriteModFile(bytes, dst, ApplicationSource, destItem, index, modlist);
                }

                if (ProgressReporter != null)
                {
                    ProgressReporter.Report("Copying textures...");
                }

                // Raw Copy all Texture files to the new destinations to avoid having the MTRL save functions auto-generate blank textures.
                foreach (var kv in newTexturePaths)
                {
                    var src = kv.Key;
                    var dst = kv.Value;

                    await _dat.CopyFile(src, dst, ApplicationSource, true, destItem, index, modlist);
                }


                if (ProgressReporter != null)
                {
                    ProgressReporter.Report("Copying materials...");
                }
                HashSet <string> CopiedMaterials = new HashSet <string>();
                // Load every Material file and edit the texture references to the new texture paths.
                foreach (var kv in newMaterialPaths)
                {
                    var src = kv.Key;
                    var dst = kv.Value;
                    try
                    {
                        var offset = index.Get8xDataOffset(src);
                        if (offset == 0)
                        {
                            continue;
                        }
                        var xivMtrl = await _mtrl.GetMtrlData(offset, src, 11);

                        xivMtrl.MTRLPath = dst;

                        for (int i = 0; i < xivMtrl.TexturePathList.Count; i++)
                        {
                            foreach (var tkv in newTexturePaths)
                            {
                                xivMtrl.TexturePathList[i] = xivMtrl.TexturePathList[i].Replace(tkv.Key, tkv.Value);
                            }
                        }

                        await _mtrl.ImportMtrl(xivMtrl, destItem, ApplicationSource, index, modlist);

                        CopiedMaterials.Add(dst);
                    }
                    catch (Exception ex)
                    {
                        // Let functions later handle this mtrl then.
                    }
                }

                if (ProgressReporter != null)
                {
                    ProgressReporter.Report("Copying VFX...");
                }
                // Copy VFX files.
                foreach (var kv in newAvfxPaths)
                {
                    var src = kv.Key;
                    var dst = kv.Value;

                    await _dat.CopyFile(src, dst, ApplicationSource, true, destItem, index, modlist);
                }

                if (ProgressReporter != null)
                {
                    ProgressReporter.Report("Creating missing variants...");
                }
                // Check to see if we need to add any variants
                var cloneNum = newMetadata.ImcEntries.Count >= 2 ? 1 : 0;
                while (originalDestinationMetadata.ImcEntries.Count > newMetadata.ImcEntries.Count)
                {
                    // Clone Variant 1 into the variants we are missing.
                    newMetadata.ImcEntries.Add((XivImc)newMetadata.ImcEntries[cloneNum].Clone());
                }


                if (singleVariant >= 0)
                {
                    if (ProgressReporter != null)
                    {
                        ProgressReporter.Report("Setting single-variant data...");
                    }

                    if (singleVariant < newMetadata.ImcEntries.Count)
                    {
                        var v = newMetadata.ImcEntries[singleVariant];

                        for (int i = 0; i < newMetadata.ImcEntries.Count; i++)
                        {
                            newMetadata.ImcEntries[i] = (XivImc)v.Clone();
                        }
                    }
                }

                // Update Skeleton references to be for the correct set Id.
                var setId = Destination.Info.SecondaryId == null ? (ushort)Destination.Info.PrimaryId : (ushort)Destination.Info.SecondaryId;
                foreach (var entry in newMetadata.EstEntries)
                {
                    entry.Value.SetId = setId;
                }

                if (ProgressReporter != null)
                {
                    ProgressReporter.Report("Copying metdata...");
                }

                // Poke through the variants and adjust any that point to null Material Sets to instead use a valid one.
                if (newMetadata.ImcEntries.Count > 0 && originalMetadata.ImcEntries.Count > 0)
                {
                    var valid = newMetadata.ImcEntries.FirstOrDefault(x => x.MaterialSet != 0).MaterialSet;
                    if (valid <= 0)
                    {
                        valid = originalMetadata.ImcEntries.FirstOrDefault(x => x.MaterialSet != 0).MaterialSet;
                    }

                    for (int i = 0; i < newMetadata.ImcEntries.Count; i++)
                    {
                        var entry = newMetadata.ImcEntries[i];
                        if (entry.MaterialSet == 0)
                        {
                            entry.MaterialSet = valid;
                        }
                    }
                }

                await ItemMetadata.SaveMetadata(newMetadata, ApplicationSource, index, modlist);

                // Save the new Metadata file via the batch function so that it's only written to the memory cache for now.
                await ItemMetadata.ApplyMetadataBatched(new List <ItemMetadata>() { newMetadata }, index, modlist, false);



                if (ProgressReporter != null)
                {
                    ProgressReporter.Report("Filling in missing material sets...");
                }
                // Validate all variants/material sets for valid materials, and copy materials as needed to fix.
                if (Imc.UsesImc(Destination))
                {
                    var mSets = newMetadata.ImcEntries.Select(x => x.MaterialSet).Distinct();
                    foreach (var mSetId in mSets)
                    {
                        var path = Destination.Info.GetRootFolder() + "material/v" + mSetId.ToString().PadLeft(4, '0') + "/";
                        foreach (var mkv in newMaterialFileNames)
                        {
                            // See if the material was copied over.
                            var destPath = path + mkv.Value;
                            if (CopiedMaterials.Contains(destPath))
                            {
                                continue;
                            }

                            string existentCopy = null;

                            // If not, find a material where one *was* copied over.
                            foreach (var mSetId2 in mSets)
                            {
                                var p2 = Destination.Info.GetRootFolder() + "material/v" + mSetId2.ToString().PadLeft(4, '0') + "/";
                                foreach (var cmat2 in CopiedMaterials)
                                {
                                    if (cmat2 == p2 + mkv.Value)
                                    {
                                        existentCopy = cmat2;
                                        break;
                                    }
                                }
                            }

                            // Shouldn't ever actually hit this, but if we do, nothing to be done about it.
                            if (existentCopy == null)
                            {
                                continue;
                            }

                            // Copy the material over.
                            await _dat.CopyFile(existentCopy, destPath, ApplicationSource, true, destItem, index, modlist);
                        }
                    }
                }

                if (ProgressReporter != null)
                {
                    ProgressReporter.Report("Updating modlist...");
                }

                if (modPack == null)
                {
                    modPack = new ModPack()
                    {
                        author = "System", name = "Item Copy - " + srcItem.Name + " to " + iName, url = "", version = "1.0"
                    };
                }

                List <Mod> mods = new List <Mod>();
                foreach (var mod in modlist.Mods)
                {
                    if (allFiles.Contains(mod.fullPath))
                    {
                        // Ensure all of our modified files are attributed correctly.
                        mod.name     = iName;
                        mod.category = iCat;
                        mod.source   = ApplicationSource;
                        mod.modPack  = modPack;

                        mods.Add(mod);
                    }
                }

                if (!modlist.ModPacks.Any(x => x.name == modPack.name))
                {
                    modlist.ModPacks.Add(modPack);
                }

                if (doSave)
                {
                    // Save everything.
                    await _index.SaveIndexFile(index);

                    await _modding.SaveModListAsync(modlist);
                }

                XivCache.QueueDependencyUpdate(allFiles.ToList());

                if (saveDirectory != null)
                {
                    ProgressReporter.Report("Creating TTMP File...");
                    var desc = "Item Converter Modpack - " + srcItem.Name + " -> " + iName + "\nCreated at: " + DateTime.Now.ToString();
                    // Time to save the modlist to file.
                    var dir   = new DirectoryInfo(saveDirectory);
                    var _ttmp = new TTMP(dir, ApplicationSource);
                    var smpd  = new SimpleModPackData()
                    {
                        Author            = modPack.author,
                        Description       = desc,
                        Url               = modPack.url,
                        Version           = new Version(1, 0, 0),
                        Name              = modPack.name,
                        SimpleModDataList = new List <SimpleModData>()
                    };

                    foreach (var mod in mods)
                    {
                        var size = await _dat.GetCompressedFileSize(mod.data.modOffset, df);

                        var smd = new SimpleModData()
                        {
                            Name      = iName,
                            FullPath  = mod.fullPath,
                            DatFile   = df.GetDataFileName(),
                            Category  = iCat,
                            IsDefault = false,
                            ModSize   = size,
                            ModOffset = mod.data.modOffset
                        };
                        smpd.SimpleModDataList.Add(smd);
                    }

                    await _ttmp.CreateSimpleModPack(smpd, XivCache.GameInfo.GameDirectory, null, true);
                }



                if (ProgressReporter != null)
                {
                    ProgressReporter.Report("Root copy complete.");
                }

                // Return the final file conversion listing.
                var ret  = newModelPaths.Union(newMaterialPaths).Union(newAvfxPaths).Union(newTexturePaths);
                var dict = ret.ToDictionary(x => x.Key, x => x.Value);
                dict.Add(Source.Info.GetRootFile(), Destination.Info.GetRootFile());
                return(dict);
            } finally
            {
                XivCache.CacheWorkerEnabled = workerStatus;
            }
        }
        /// <summary>
        /// Applies multiple metadata mods simultaneously for performance gains.
        /// </summary>
        /// <param name="data"></param>
        /// <param name="index"></param>
        /// <param name="modlist"></param>
        /// <returns></returns>
        internal static async Task ApplyMetadataBatched(List <ItemMetadata> data, IndexFile index, ModList modlist, bool save = true)
        {
            if (data == null || data.Count == 0)
            {
                return;
            }

            var _eqp     = new Eqp(XivCache.GameInfo.GameDirectory);
            var _modding = new Modding(XivCache.GameInfo.GameDirectory);
            var _index   = new Index(XivCache.GameInfo.GameDirectory);

            var dummyItem = new XivGenericItemModel();

            dummyItem.Name = Constants.InternalModSourceName;
            dummyItem.SecondaryCategory = Constants.InternalModSourceName;

            Dictionary <XivRace, List <(uint PrimaryId, string Slot, EquipmentDeformationParameter Entry)> > eqdpEntries = new Dictionary <XivRace, List <(uint PrimaryId, string Slot, EquipmentDeformationParameter Entry)> >();
            Dictionary <Est.EstType, List <ExtraSkeletonEntry> > estEntries = new Dictionary <Est.EstType, List <ExtraSkeletonEntry> >();
            List <(uint PrimaryId, EquipmentParameter EqpData)>  eqpEntries = new List <(uint PrimaryId, EquipmentParameter EqpData)>();
            List <(uint PrimaryId, GimmickParameter GmpData)>    gmpEntries = new List <(uint PrimaryId, GimmickParameter GmpData)>();

            foreach (var meta in data)
            {
                // Construct the parameter collections for each function call.
                foreach (var kv in meta.EqdpEntries)
                {
                    if (!eqdpEntries.ContainsKey(kv.Key))
                    {
                        eqdpEntries.Add(kv.Key, new List <(uint PrimaryId, string Slot, EquipmentDeformationParameter Entry)>());
                    }

                    eqdpEntries[kv.Key].Add(((uint)meta.Root.Info.PrimaryId, meta.Root.Info.Slot, kv.Value));
                }

                var estType = Est.GetEstType(meta.Root);
                foreach (var kv in meta.EstEntries)
                {
                    if (!estEntries.ContainsKey(estType))
                    {
                        estEntries.Add(estType, new List <ExtraSkeletonEntry>());
                    }

                    estEntries[estType].Add(kv.Value);
                }

                if (meta.EqpEntry != null)
                {
                    eqpEntries.Add(((uint)meta.Root.Info.PrimaryId, meta.EqpEntry));
                }

                if (meta.GmpEntry != null)
                {
                    gmpEntries.Add(((uint)meta.Root.Info.PrimaryId, meta.GmpEntry));
                }
            }


            if (index.DataFile == XivDataFile._04_Chara)
            {
                // Batch install functions for these three.
                await _eqp.SaveEqpEntries(eqpEntries, dummyItem, index, modlist);

                await _eqp.SaveEqdpEntries(eqdpEntries, dummyItem, index, modlist);

                await _eqp.SaveGmpEntries(gmpEntries, dummyItem, index, modlist);

                // The EST function already does batch applications by nature of how it works,
                // so just call it once for each of the four EST types represented.
                foreach (var kv in estEntries)
                {
                    await Est.SaveExtraSkeletonEntries(kv.Key, kv.Value, dummyItem, index, modlist);
                }
            }


            // IMC Files don't really overlap that often, so it's
            // not a significant loss generally to just write them individually.
            foreach (var meta in data)
            {
                if (meta.ImcEntries.Count > 0)
                {
                    var _imc    = new Imc(XivCache.GameInfo.GameDirectory);
                    var imcPath = meta.Root.GetRawImcFilePath();
                    await _imc.SaveEntries(imcPath, meta.Root.Info.Slot, meta.ImcEntries, null, index, modlist);
                }
            }

            if (save)
            {
                await _index.SaveIndexFile(index);

                await _modding.SaveModListAsync(modlist);
            }
        }