private static void CompareHash(int local, int game, Utf8GamePath path)
 {
     if (local != game)
     {
         PluginLog.Warning("Hash function appears to have changed. Computed {Hash1:X8} vs Game {Hash2:X8} for {Path}.", local, game, path);
     }
 }
Example #2
0
    // Version 2 mod packs can either be simple or extended, import accordingly.
    private DirectoryInfo ImportV2ModPack(FileInfo _, ZipFile extractedModPack, string modRaw)
    {
        var modList = JsonConvert.DeserializeObject <SimpleModPack>(modRaw, JsonSettings) !;

        if (modList.TtmpVersion.EndsWith("s"))
        {
            return(ImportSimpleV2ModPack(extractedModPack, modList));
        }

        if (modList.TtmpVersion.EndsWith("w"))
        {
            return(ImportExtendedV2ModPack(extractedModPack, modRaw));
        }

        try
        {
            PluginLog.Warning($"Unknown TTMPVersion <{modList.TtmpVersion}> given, trying to export as simple mod pack.");
            return(ImportSimpleV2ModPack(extractedModPack, modList));
        }
        catch (Exception e1)
        {
            PluginLog.Warning($"Exporting as simple mod pack failed with following error, retrying as extended mod pack:\n{e1}");
            try
            {
                return(ImportExtendedV2ModPack(extractedModPack, modRaw));
            }
            catch (Exception e2)
            {
                throw new IOException("Exporting as extended mod pack failed, too. Version unsupported or file defect.", e2);
            }
        }
    }
Example #3
0
        public IReadOnlyDictionary <string, object?> GetChangedItemsForCollection(string collectionName)
        {
            CheckInitialized();
            try
            {
                var modManager = Service <ModManager> .Get();

                if (!modManager.Collections.Collections.TryGetValue(collectionName, out var collection))
                {
                    collection = ModCollection.Empty;
                }

                if (collection.Cache != null)
                {
                    return(collection.Cache.ChangedItems);
                }

                PluginLog.Warning($"Collection {collectionName} does not exist or is not loaded.");
                return(new Dictionary <string, object?>());
            }
            catch (Exception e)
            {
                PluginLog.Error($"Could not obtain Changed Items for {collectionName}:\n{e}");
                throw;
            }
        }
        public static MultiModGroup?Load(JObject json, DirectoryInfo basePath)
        {
            var options = json["Options"];
            var ret     = new MultiModGroup()
            {
                Name        = json[nameof(Name)]?.ToObject <string>() ?? string.Empty,
                Description = json[nameof(Description)]?.ToObject <string>() ?? string.Empty,
                Priority    = json[nameof(Priority)]?.ToObject <int>() ?? 0,
            };

            if (ret.Name.Length == 0)
            {
                return(null);
            }

            if (options != null)
            {
                foreach (var child in options.Children())
                {
                    if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions)
                    {
                        PluginLog.Warning($"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.");
                        break;
                    }
                    var subMod = new SubMod();
                    subMod.Load(basePath, child, out var priority);
                    ret.PrioritizedOptions.Add((subMod, priority));
                }
            }

            return(ret);
        }
Example #5
0
        private void VerifyVersionAndImport(FileInfo modPackFile)
        {
            using var zfs = modPackFile.OpenRead();
            using var extractedModPack = new ZipFile(zfs);

            var mpl = FindZipEntry(extractedModPack, "TTMPL.mpl");

            if (mpl == null)
            {
                throw new FileNotFoundException("ZIP does not contain a TTMPL.mpl file.");
            }

            var modRaw = GetStringFromZipEntry(extractedModPack, mpl, Encoding.UTF8);

            // At least a better validation than going by the extension.
            if (modRaw.Contains("\"TTMPVersion\":"))
            {
                if (modPackFile.Extension != ".ttmp2")
                {
                    PluginLog.Warning($"File {modPackFile.FullName} seems to be a V2 TTMP, but has the wrong extension.");
                }

                ImportV2ModPack(modPackFile, extractedModPack, modRaw);
            }
            else
            {
                if (modPackFile.Extension != ".ttmp")
                {
                    PluginLog.Warning($"File {modPackFile.FullName} seems to be a V1 TTMP, but has the wrong extension.");
                }

                ImportV1ModPack(modPackFile, extractedModPack, modRaw);
            }
        }
        /// <summary>
        /// Initializes a new instance of the <see cref="ContextMenuReaderWriter"/> class.
        /// </summary>
        /// <param name="agentContextInterface">The AgentContextInterface to act upon.</param>
        /// <param name="atkValueCount">The number of ATK values to consider.</param>
        /// <param name="atkValues">Pointer to the array of ATK values.</param>
        public ContextMenuReaderWriter(AgentContextInterface *agentContextInterface, int atkValueCount, AtkValue *atkValues)
        {
            PluginLog.Warning($"{(IntPtr)atkValues:X}");

            this.agentContextInterface = agentContextInterface;
            this.atkValueCount         = atkValueCount;
            this.atkValues             = atkValues;
        }
Example #7
0
        // Draw the edit tab that contains all things concerning editing the mod.
        private void DrawEditModTab()
        {
            using var tab = DrawTab(EditModTabHeader, Tabs.Edit);
            if (!tab)
            {
                return;
            }

            using var child = ImRaii.Child("##editChild", -Vector2.One);
            if (!child)
            {
                return;
            }

            _cellPadding = ImGui.GetStyle().CellPadding with {
                X = 2 * ImGuiHelpers.GlobalScale
            };
            _itemSpacing = ImGui.GetStyle().CellPadding with {
                X = 4 * ImGuiHelpers.GlobalScale
            };

            EditButtons();
            EditRegularMeta();
            ImGui.Dummy(_window._defaultSpace);

            if (Input.Text("Mod Path", Input.Path, Input.None, _leaf.FullName(), out var newPath, 256,
                           _window._inputTextWidth.X))
            {
                try
                {
                    _window._penumbra.ModFileSystem.RenameAndMove(_leaf, newPath);
                }
                catch (Exception e)
                {
                    PluginLog.Warning(e.Message);
                }
            }

            ImGui.Dummy(_window._defaultSpace);
            AddOptionGroup.Draw(_window, _mod);
            ImGui.Dummy(_window._defaultSpace);

            for (var groupIdx = 0; groupIdx < _mod.Groups.Count; ++groupIdx)
            {
                EditGroup(groupIdx);
            }

            EndActions();
            DescriptionEdit.DrawPopup(_window);
        }
Example #8
0
        // Add a new collection of the given name.
        // If duplicate is not-null, the new collection will be a duplicate of it.
        // If the name of the collection would result in an already existing filename, skip it.
        // Returns true if the collection was successfully created and fires a Inactive event.
        // Also sets the current collection to the new collection afterwards.
        public bool AddCollection(string name, ModCollection?duplicate)
        {
            if (!CanAddCollection(name, out var fixedName))
            {
                PluginLog.Warning($"The new collection {name} would lead to the same path {fixedName} as one that already exists.");
                return(false);
            }

            var newCollection = duplicate?.Duplicate(name) ?? CreateNewEmpty(name);

            newCollection.Index = _collections.Count;
            _collections.Add(newCollection);
            newCollection.Save();
            PluginLog.Debug("Added collection {Name:l}.", newCollection.Name);
            CollectionChanged.Invoke(Type.Inactive, null, newCollection);
            SetCollection(newCollection.Index, Type.Current);
            return(true);
        }
Example #9
0
        private T?GetFileIntern <T>(string resolvedPath) where T : FileResource
        {
            CheckInitialized();
            try
            {
                if (Path.IsPathRooted(resolvedPath))
                {
                    return(_lumina?.GetFileFromDisk <T>(resolvedPath));
                }

                return(Dalamud.GameData.GetFile <T>(resolvedPath));
            }
            catch (Exception e)
            {
                PluginLog.Warning($"Could not load file {resolvedPath}:\n{e}");
                return(null);
            }
        }
Example #10
0
        public static bool VerifyFileName(Mod mod, IModGroup?group, string newName, bool message)
        {
            var path = newName.RemoveInvalidPathSymbols();

            if (path.Length == 0 ||
                mod.Groups.Any(o => !ReferenceEquals(o, group) &&
                               string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.InvariantCultureIgnoreCase)))
            {
                if (message)
                {
                    PluginLog.Warning($"Could not name option {newName} because option with same filename {path} already exists.");
                }

                return(false);
            }

            return(true);
        }
        public SkipCutscene()
        {
            if (Interface.GetPluginConfig() is not Config configuration || configuration.Version == 0)
            {
                configuration = new Config {
                    IsEnabled = true, Version = 1
                }
            }
            ;

            _config = configuration;

            Address = new CutsceneAddressResolver();

            Address.Setup(SigScanner);

            if (Address.Offset1 != IntPtr.Zero && Address.Offset2 != IntPtr.Zero)
            {
                PluginLog.Information("Cutscene Offset Found.");
                if (_config.IsEnabled)
                {
                    SetEnabled(true);
                }
            }
            else
            {
                PluginLog.Error("Cutscene Offset Not Found.");
                PluginLog.Warning("Plugin Disabling...");
                Dispose();
                return;
            }

            _csp = new RNGCryptoServiceProvider();

            CommandManager.AddHandler("/sc", new CommandInfo(OnCommand)
            {
                HelpMessage = "/sc: Roll your sanity check dice."
            });
        }
Example #12
0
        // Reload a mod without changing its base directory.
        // If the base directory does not exist anymore, the mod will be deleted.
        public void ReloadMod(int idx)
        {
            var mod     = this[idx];
            var oldName = mod.Name;

            ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath);
            if (!mod.Reload(out var metaChange))
            {
                PluginLog.Warning(mod.Name.Length == 0
                    ? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead."
                    : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead.");

                DeleteMod(idx);
                return;
            }

            ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath);
            if (metaChange != MetaChangeType.None)
            {
                ModMetaChanged?.Invoke(metaChange, mod, oldName);
            }
        }
Example #13
0
    public IReadOnlyDictionary <string, object?> GetChangedItemsForCollection(string collectionName)
    {
        CheckInitialized();
        try
        {
            if (!Penumbra.CollectionManager.ByName(collectionName, out var collection))
            {
                collection = ModCollection.Empty;
            }

            if (collection.HasCache)
            {
                return(collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2));
            }

            PluginLog.Warning($"Collection {collectionName} does not exist or is not loaded.");
            return(new Dictionary <string, object?>());
        }
        catch (Exception e)
        {
            PluginLog.Error($"Could not obtain Changed Items for {collectionName}:\n{e}");
            throw;
        }
    }
Example #14
0
        /// <summary>
        /// Initialises an object's fields and properties that are annotated with a
        /// <see cref="SignatureAttribute"/>.
        /// </summary>
        /// <param name="self">The object to initialise.</param>
        /// <param name="log">If warnings should be logged using <see cref="PluginLog"/>.</param>
        public static void Initialise(object self, bool log = true)
        {
            var scanner = Service <SigScanner> .Get();

            var selfType = self.GetType();
            var fields   = selfType.GetFields(Flags).Select(field => (IFieldOrPropertyInfo) new FieldInfoWrapper(field))
                           .Concat(selfType.GetProperties(Flags).Select(prop => new PropertyInfoWrapper(prop)))
                           .Select(field => (field, field.GetCustomAttribute <SignatureAttribute>()))
                           .Where(field => field.Item2 != null);

            foreach (var(info, sig) in fields)
            {
                var wasWrapped = false;
                var actualType = info.ActualType;
                if (actualType.IsGenericType && actualType.GetGenericTypeDefinition() == typeof(Nullable <>))
                {
                    // unwrap the nullable
                    actualType = actualType.GetGenericArguments()[0];
                    wasWrapped = true;
                }

                var fallibility = sig !.Fallibility;
                if (fallibility == Fallibility.Auto)
                {
                    fallibility = info.IsNullable || wasWrapped
                                      ? Fallibility.Fallible
                                      : Fallibility.Infallible;
                }

                var fallible = fallibility == Fallibility.Fallible;

                void Invalid(string message, bool prepend = true)
                {
                    var errorMsg = prepend
                                       ? $"Invalid Signature attribute for {selfType.FullName}.{info.Name}: {message}"
                                       : message;

                    if (fallible)
                    {
                        PluginLog.Warning(errorMsg);
                    }
                    else
                    {
                        throw new SignatureException(errorMsg);
                    }
                }

                IntPtr ptr;
                var    success = sig.ScanType == ScanType.Text
                                  ? scanner.TryScanText(sig.Signature, out ptr)
                                  : scanner.TryGetStaticAddressFromSig(sig.Signature, out ptr);
                if (!success)
                {
                    if (log)
                    {
                        Invalid($"Failed to find {sig.ScanType} signature \"{info.Name}\" for {selfType.FullName} ({sig.Signature})", false);
                    }

                    continue;
                }

                switch (sig.UseFlags)
                {
                case SignatureUseFlags.Auto when actualType == typeof(IntPtr) || actualType.IsPointer || actualType.IsAssignableTo(typeof(Delegate)):
                case SignatureUseFlags.Pointer:
                {
                    if (actualType.IsAssignableTo(typeof(Delegate)))
                    {
                        info.SetValue(self, Marshal.GetDelegateForFunctionPointer(ptr, actualType));
                    }
                    else
                    {
                        info.SetValue(self, ptr);
                    }

                    break;
                }

                case SignatureUseFlags.Auto when actualType.IsGenericType && actualType.GetGenericTypeDefinition() == typeof(Hook <>):
                case SignatureUseFlags.Hook:
                {
                    if (!actualType.IsGenericType || actualType.GetGenericTypeDefinition() != typeof(Hook <>))
                    {
                        Invalid($"{actualType.Name} is not a Hook<T>");
                        continue;
                    }

                    var hookDelegateType = actualType.GenericTypeArguments[0];

                    Delegate?detour;
                    if (sig.DetourName == null)
                    {
                        var matches = selfType.GetMethods(Flags)
                                      .Select(method => method.IsStatic
                                                                        ? Delegate.CreateDelegate(hookDelegateType, method, false)
                                                                        : Delegate.CreateDelegate(hookDelegateType, self, method, false))
                                      .Where(del => del != null)
                                      .ToArray();
                        if (matches.Length != 1)
                        {
                            Invalid("Either found no matching detours or found more than one: specify a detour name");
                            continue;
                        }

                        detour = matches[0] !;
                    }
                    else
                    {
                        var method = selfType.GetMethod(sig.DetourName, Flags);
                        if (method == null)
                        {
                            Invalid($"Could not find detour \"{sig.DetourName}\"");
                            continue;
                        }

                        var del = method.IsStatic
                                          ? Delegate.CreateDelegate(hookDelegateType, method, false)
                                          : Delegate.CreateDelegate(hookDelegateType, self, method, false);
                        if (del == null)
                        {
                            Invalid($"Method {sig.DetourName} was not compatible with delegate {hookDelegateType.Name}");
                            continue;
                        }

                        detour = del;
                    }

                    var ctor = actualType.GetConstructor(new[] { typeof(IntPtr), hookDelegateType });
                    if (ctor == null)
                    {
                        PluginLog.Error("Error in SignatureHelper: could not find Hook constructor");
                        continue;
                    }

                    var hook = ctor.Invoke(new object?[] { ptr, detour });
                    info.SetValue(self, hook);

                    break;
                }

                case SignatureUseFlags.Auto when actualType.IsPrimitive:
                case SignatureUseFlags.Offset:
                {
                    var offset = Marshal.PtrToStructure(ptr + sig.Offset, actualType);
                    info.SetValue(self, offset);

                    break;
                }

                default:
                {
                    if (log)
                    {
                        Invalid("could not detect desired signature use, set SignatureUseFlags manually");
                    }

                    break;
                }
                }
            }
        }
Example #15
0
        private unsafe ContextMenuOpenedArgs?NotifyContextMenuOpened(AddonContextMenu *addonContextMenu, AgentContextInterface *agentContextInterface, string?title, ContextMenus.ContextMenuOpenedDelegate contextMenuOpenedDelegate, IEnumerable <ContextMenuItem> initialContextMenuItems)
        {
            var parentAddonName = this.GetParentAddonName(&addonContextMenu->AtkUnitBase);

            Log.Warning($"AgentContextInterface at: {new IntPtr(agentContextInterface):X}");

            InventoryItemContext?inventoryItemContext = null;
            GameObjectContext?   gameObjectContext    = null;

            if (IsInventoryContext(agentContextInterface))
            {
                var agentInventoryContext = (AgentInventoryContext *)agentContextInterface;
                inventoryItemContext = new InventoryItemContext(agentInventoryContext->InventoryItemId, agentInventoryContext->InventoryItemCount, agentInventoryContext->InventoryItemIsHighQuality);
            }
            else
            {
                var agentContext = (AgentContext *)agentContextInterface;

                uint?id = agentContext->GameObjectId;
                if (id == 0)
                {
                    id = null;
                }

                ulong?contentId = agentContext->GameObjectContentId;
                if (contentId == 0)
                {
                    contentId = null;
                }

                var name = MemoryHelper.ReadSeStringNullTerminated((IntPtr)agentContext->GameObjectName.StringPtr).TextValue;
                if (string.IsNullOrEmpty(name))
                {
                    name = null;
                }

                ushort?worldId = agentContext->GameObjectWorldId;
                if (worldId == 0)
                {
                    worldId = null;
                }

                if (id != null ||
                    contentId != null ||
                    name != null ||
                    worldId != null)
                {
                    gameObjectContext = new GameObjectContext(id, contentId, name, worldId);
                }
            }

            // Temporarily remove the < Return item, for UX we should enforce that it is always last in the list.
            var lastContextMenuItem = initialContextMenuItems.LastOrDefault();

            if (lastContextMenuItem is GameContextMenuItem gameContextMenuItem && gameContextMenuItem.SelectedAction == 102)
            {
                initialContextMenuItems = initialContextMenuItems.SkipLast(1);
            }

            var contextMenuOpenedArgs = new ContextMenuOpenedArgs(addonContextMenu, agentContextInterface, parentAddonName, initialContextMenuItems)
            {
                Title = title,
                InventoryItemContext = inventoryItemContext,
                GameObjectContext    = gameObjectContext,
            };

            try
            {
                contextMenuOpenedDelegate.Invoke(contextMenuOpenedArgs);
            }
            catch (Exception ex)
            {
                PluginLog.LogError(ex, "NotifyContextMenuOpened");
                return(null);
            }

            // Readd the < Return item
            if (lastContextMenuItem is GameContextMenuItem gameContextMenuItem1 && gameContextMenuItem1.SelectedAction == 102)
            {
                contextMenuOpenedArgs.Items.Add(lastContextMenuItem);
            }

            foreach (var contextMenuItem in contextMenuOpenedArgs.Items.ToArray())
            {
                // TODO: Game doesn't support nested sub context menus, but we might be able to.
                if (contextMenuItem is OpenSubContextMenuItem && contextMenuOpenedArgs.Title != null)
                {
                    contextMenuOpenedArgs.Items.Remove(contextMenuItem);
                    PluginLog.Warning($"Context menu '{contextMenuOpenedArgs.Title}' item '{contextMenuItem}' has been removed because nested sub context menus are not supported.");
                }
            }

            if (contextMenuOpenedArgs.Items.Count > MaxContextMenuItemsPerContextMenu)
            {
                PluginLog.LogWarning($"Context menu requesting {contextMenuOpenedArgs.Items.Count} of max {MaxContextMenuItemsPerContextMenu} items. Resizing list to compensate.");
                contextMenuOpenedArgs.Items.RemoveRange(MaxContextMenuItemsPerContextMenu, contextMenuOpenedArgs.Items.Count - MaxContextMenuItemsPerContextMenu);
            }

            return(contextMenuOpenedArgs);
        }
Example #16
0
        private unsafe void ContextMenuOpenedImplementation(AddonContextMenu *addonContextMenu, ref int atkValueCount, ref AtkValue *atkValues)
        {
            if (this.ContextMenuOpened == null ||
                this.currentAgentContextInterface == null)
            {
                return;
            }

            var contextMenuReaderWriter = new ContextMenuReaderWriter(this.currentAgentContextInterface, atkValueCount, atkValues);

            // Check for a title.
            string?title = null;

            if (this.selectedOpenSubContextMenuItem != null)
            {
                title = this.selectedOpenSubContextMenuItem.Name.TextValue;

                // Write the custom title
                var titleAtkValue = &atkValues[1];
                fixed(byte *titlePtr = this.selectedOpenSubContextMenuItem.Name.Encode().NullTerminate())
                {
                    titleAtkValue->SetString(titlePtr);
                }
            }
            else if (contextMenuReaderWriter.Title != null)
            {
                title = contextMenuReaderWriter.Title.TextValue;
            }

            // Determine which event to raise.
            var contextMenuOpenedDelegate = this.ContextMenuOpened;

            // this.selectedOpenSubContextMenuItem is OpenSubContextMenuItem openSubContextMenuItem
            if (this.selectedOpenSubContextMenuItem != null)
            {
                contextMenuOpenedDelegate = this.selectedOpenSubContextMenuItem.Opened;
            }

            // Get the existing items from the game.
            // TODO: For inventory sub context menus, we take only the last item -- the return item.
            // This is because we're doing a hack to spawn a Second Tier sub context menu and then appropriating it.
            var contextMenuItems = contextMenuReaderWriter.Read();

            if (IsInventoryContext(this.currentAgentContextInterface) && this.selectedOpenSubContextMenuItem != null)
            {
                contextMenuItems = contextMenuItems.TakeLast(1).ToArray();
            }

            var beforeHashCode = GetContextMenuItemsHashCode(contextMenuItems);

            // Raise the event and get the context menu changes.
            this.currentContextMenuOpenedArgs = this.NotifyContextMenuOpened(addonContextMenu, this.currentAgentContextInterface, title, contextMenuOpenedDelegate, contextMenuItems);
            if (this.currentContextMenuOpenedArgs == null)
            {
                return;
            }

            var afterHashCode = GetContextMenuItemsHashCode(this.currentContextMenuOpenedArgs.Items);

            PluginLog.Warning($"{beforeHashCode}={afterHashCode}");

            // Only write to memory if the items were actually changed.
            if (beforeHashCode != afterHashCode)
            {
                // Write the new changes.
                contextMenuReaderWriter.Write(this.currentContextMenuOpenedArgs.Items);

                // Update the addon.
                atkValueCount = *(&addonContextMenu->AtkValuesCount) = (ushort)contextMenuReaderWriter.AtkValueCount;
                atkValues     = *(&addonContextMenu->AtkValues) = contextMenuReaderWriter.AtkValues;
            }
        }
Example #17
0
        private static bool MigrateV0ToV1(Mod mod, JObject json)
        {
            if (mod.FileVersion > 0)
            {
                return(false);
            }

            var swaps = json["FileSwaps"]?.ToObject <Dictionary <Utf8GamePath, FullPath> >()
                        ?? new Dictionary <Utf8GamePath, FullPath>();
            var groups        = json["Groups"]?.ToObject <Dictionary <string, OptionGroupV0> >() ?? new Dictionary <string, OptionGroupV0>();
            var priority      = 1;
            var seenMetaFiles = new HashSet <FullPath>();

            foreach (var group in groups.Values)
            {
                ConvertGroup(mod, group, ref priority, seenMetaFiles);
            }

            foreach (var unusedFile in mod.FindUnusedFiles().Where(f => !seenMetaFiles.Contains(f)))
            {
                if (unusedFile.ToGamePath(mod.ModPath, out var gamePath) &&
                    !mod._default.FileData.TryAdd(gamePath, unusedFile))
                {
                    PluginLog.Error($"Could not add {gamePath} because it already points to {mod._default.FileData[ gamePath ]}.");
                }
            }

            mod._default.FileSwapData.Clear();
            mod._default.FileSwapData.EnsureCapacity(swaps.Count);
            foreach (var(gamePath, swapPath) in swaps)
            {
                mod._default.FileSwapData.Add(gamePath, swapPath);
            }

            mod._default.IncorporateMetaChanges(mod.ModPath, true);
            foreach (var(group, index) in mod.Groups.WithIndex())
            {
                IModGroup.Save(group, mod.ModPath, index);
            }

            // Delete meta files.
            foreach (var file in seenMetaFiles.Where(f => f.Exists))
            {
                try
                {
                    File.Delete(file.FullName);
                }
                catch (Exception e)
                {
                    PluginLog.Warning($"Could not delete meta file {file.FullName} during migration:\n{e}");
                }
            }

            // Delete old meta files.
            var oldMetaFile = Path.Combine(mod.ModPath.FullName, "metadata_manipulations.json");

            if (File.Exists(oldMetaFile))
            {
                try
                {
                    File.Delete(oldMetaFile);
                }
                catch (Exception e)
                {
                    PluginLog.Warning($"Could not delete old meta file {oldMetaFile} during migration:\n{e}");
                }
            }

            mod.FileVersion = 1;
            mod.SaveDefaultMod();
            mod.SaveMeta();

            return(true);
        }