diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 8974e823..5ce28510 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -877,7 +877,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc CreateNamedTemporaryCollection(string name) { CheckInitialized(); - if (name.Length == 0 || ModCreator.ReplaceBadXivSymbols(name) != name || name.Contains('|')) + if (name.Length == 0 || ModCreator.ReplaceBadXivSymbols(name, _config.ReplaceNonAsciiOnImport) != name || name.Contains('|')) return PenumbraApiEc.InvalidArgument; return _tempCollections.CreateTemporaryCollection(name).Length > 0 diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 8f21ee52..a5a615bd 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -46,6 +46,7 @@ public class Configuration : IPluginConfiguration, ISavable public bool UseOwnerNameForCharacterCollection { get; set; } = true; public bool UseNoModsInInspect { get; set; } = false; public bool HideChangedItemFilters { get; set; } = false; + public bool ReplaceNonAsciiOnImport { get; set; } = false; public bool HidePrioritiesInSelector { get; set; } = false; public bool HideRedrawBar { get; set; } = false; diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index 6ddafdd7..57313ab1 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -44,7 +44,7 @@ public partial class TexToolsImporter }; Penumbra.Log.Information($" -> Importing {archive.Type} Archive."); - _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, Path.GetRandomFileName()); + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, Path.GetRandomFileName(), _config.ReplaceNonAsciiOnImport, true); var options = new ExtractionOptions() { ExtractFullPath = true, @@ -97,13 +97,13 @@ public partial class TexToolsImporter // Use either the top-level directory as the mods base name, or the (fixed for path) name in the json. if (leadDir) { - _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, baseName, false); + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, baseName, _config.ReplaceNonAsciiOnImport, false); Directory.Move(Path.Combine(oldName, baseName), _currentModDirectory.FullName); Directory.Delete(oldName); } else { - _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, name, false); + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, name, _config.ReplaceNonAsciiOnImport, false); Directory.Move(oldName, _currentModDirectory.FullName); } diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index dbe76ae3..94a5e5ac 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -35,7 +35,7 @@ public partial class TexToolsImporter var modList = modListRaw.Select(m => JsonConvert.DeserializeObject(m, JsonSettings)!).ToList(); - _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, Path.GetFileNameWithoutExtension(modPackFile.Name)); + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, Path.GetFileNameWithoutExtension(modPackFile.Name), _config.ReplaceNonAsciiOnImport, true); // Create a new ModMeta from the TTMP mod list info _modManager.DataEditor.CreateMeta(_currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null); @@ -88,7 +88,7 @@ public partial class TexToolsImporter _currentOptionName = DefaultTexToolsData.DefaultOption; Penumbra.Log.Information(" -> Importing Simple V2 ModPack"); - _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, _currentModName); + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, _currentModName, _config.ReplaceNonAsciiOnImport, true); _modManager.DataEditor.CreateMeta(_currentModDirectory, _currentModName, modList.Author, string.IsNullOrEmpty(modList.Description) ? "Mod imported from TexTools mod pack" : modList.Description, modList.Version, modList.Url); @@ -131,7 +131,7 @@ public partial class TexToolsImporter _currentNumOptions = GetOptionCount(modList); _currentModName = modList.Name; - _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, _currentModName); + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, _currentModName, _config.ReplaceNonAsciiOnImport, true); _modManager.DataEditor.CreateMeta(_currentModDirectory, _currentModName, modList.Author, modList.Description, modList.Version, modList.Url); @@ -168,7 +168,7 @@ public partial class TexToolsImporter { var name = numGroups == 1 ? _currentGroupName : $"{_currentGroupName}, Part {groupId + 1}"; options.Clear(); - var groupFolder = ModCreator.NewSubFolderName(_currentModDirectory, name) + var groupFolder = ModCreator.NewSubFolderName(_currentModDirectory, name, _config.ReplaceNonAsciiOnImport) ?? new DirectoryInfo(Path.Combine(_currentModDirectory.FullName, numGroups == 1 ? $"Group {groupPriority + 1}" : $"Group {groupPriority + 1}, Part {groupId + 1}")); @@ -178,7 +178,7 @@ public partial class TexToolsImporter var option = allOptions[i + optionIdx]; _token.ThrowIfCancellationRequested(); _currentOptionName = option.Name; - var optionFolder = ModCreator.NewSubFolderName(groupFolder, option.Name) + var optionFolder = ModCreator.NewSubFolderName(groupFolder, option.Name, _config.ReplaceNonAsciiOnImport) ?? new DirectoryInfo(Path.Combine(groupFolder.FullName, $"Option {i + optionIdx + 1}")); ExtractSimpleModList(optionFolder, option.ModsJsons); options.Add(_modManager.Creator.CreateSubMod(_currentModDirectory, optionFolder, option)); diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index d918bda2..9c42b9fc 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -49,13 +49,13 @@ public unsafe class MetaFileManager TexToolsMeta.WriteTexToolsMeta(this, mod.Default.Manipulations, mod.ModPath); foreach (var group in mod.Groups) { - var dir = ModCreator.NewOptionDirectory(mod.ModPath, group.Name); + var dir = ModCreator.NewOptionDirectory(mod.ModPath, group.Name, Config.ReplaceNonAsciiOnImport); if (!dir.Exists) dir.Create(); foreach (var option in group.OfType()) { - var optionDir = ModCreator.NewOptionDirectory(dir, option.Name); + var optionDir = ModCreator.NewOptionDirectory(dir, option.Name, Config.ReplaceNonAsciiOnImport); if (!optionDir.Exists) optionDir.Create(); diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 37ffdcfe..1dfe9e76 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -4,7 +4,6 @@ using OtterGui; using OtterGui.Classes; using Penumbra.Api.Enums; using Penumbra.Communication; -using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; using Penumbra.Services; @@ -15,6 +14,7 @@ namespace Penumbra.Mods.Editor; public class ModMerger : IDisposable { + private readonly Configuration _config; private readonly CommunicatorService _communicator; private readonly ModOptionEditor _editor; private readonly ModFileSystemSelector _selector; @@ -40,13 +40,14 @@ public class ModMerger : IDisposable public Exception? Error { get; private set; } public ModMerger(ModManager mods, ModOptionEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates, - CommunicatorService communicator, ModCreator creator) + CommunicatorService communicator, ModCreator creator, Configuration config) { _editor = editor; _selector = selector; _duplicates = duplicates; _communicator = communicator; _creator = creator; + _config = config; _mods = mods; _selector.SelectionChanged += OnSelectionChange; _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModMerger); @@ -82,7 +83,8 @@ public class ModMerger : IDisposable catch (Exception ex) { Error = ex; - Penumbra.Messager.NotificationMessage(ex, $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}, cleaning up changes.", NotificationType.Error, false); + Penumbra.Messager.NotificationMessage(ex, $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}, cleaning up changes.", + NotificationType.Error, false); FailureCleanup(); DataCleanup(); } @@ -138,10 +140,10 @@ public class ModMerger : IDisposable var (option, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName); if (optionCreated) _createdOptions.Add(option); - var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName); + var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport); if (!dir.Exists) _createdDirectories.Add(dir.FullName); - dir = ModCreator.NewOptionDirectory(dir, optionName); + dir = ModCreator.NewOptionDirectory(dir, optionName, _config.ReplaceNonAsciiOnImport); if (!dir.Exists) _createdDirectories.Add(dir.FullName); CopyFiles(dir); diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 3610c99a..c146b6f4 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -6,11 +6,10 @@ using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Editor; -public class ModNormalizer +public class ModNormalizer(ModManager _modManager, Configuration _config) { - private readonly ModManager _modManager; private readonly List>> _redirections = new(); public Mod Mod { get; private set; } = null!; @@ -25,9 +24,6 @@ public class ModNormalizer public bool Running => !Worker.IsCompleted; - public ModNormalizer(ModManager modManager) - => _modManager = modManager; - public void Normalize(Mod mod) { if (Step < TotalSteps) @@ -175,10 +171,10 @@ public class ModNormalizer for (var i = _redirections[groupIdx + 1].Count; i < group.Count; ++i) _redirections[groupIdx + 1].Add(new Dictionary()); - var groupDir = ModCreator.CreateModFolder(directory, group.Name); + var groupDir = ModCreator.CreateModFolder(directory, group.Name, _config.ReplaceNonAsciiOnImport, true); foreach (var option in group.OfType()) { - var optionDir = ModCreator.CreateModFolder(groupDir, option.Name); + var optionDir = ModCreator.CreateModFolder(groupDir, option.Name, _config.ReplaceNonAsciiOnImport, true); newDict = _redirections[groupIdx + 1][option.OptionIdx]; newDict.Clear(); @@ -228,7 +224,8 @@ public class ModNormalizer } catch (Exception e) { - Penumbra.Messager.NotificationMessage(e, $"Could not move old files out of the way while normalizing mod {Mod.Name}.", NotificationType.Error, false); + Penumbra.Messager.NotificationMessage(e, $"Could not move old files out of the way while normalizing mod {Mod.Name}.", + NotificationType.Error, false); } return false; @@ -251,7 +248,8 @@ public class ModNormalizer } catch (Exception e) { - Penumbra.Messager.NotificationMessage(e, $"Could not move new files into the mod while normalizing mod {Mod.Name}.", NotificationType.Error, false); + Penumbra.Messager.NotificationMessage(e, $"Could not move new files into the mod while normalizing mod {Mod.Name}.", + NotificationType.Error, false); foreach (var dir in Mod.ModPath.EnumerateDirectories()) { if (dir.FullName.Equals(_oldDirName, StringComparison.OrdinalIgnoreCase) diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index e258f996..40585520 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -217,7 +217,7 @@ public sealed class ModManager : ModStorage, IDisposable if (oldName == newName) return NewDirectoryState.Identical; - var fixedNewName = ModCreator.ReplaceBadXivSymbols(newName); + var fixedNewName = ModCreator.ReplaceBadXivSymbols(newName, _config.ReplaceNonAsciiOnImport); if (fixedNewName != newName) return NewDirectoryState.ContainsInvalidSymbols; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 383b6d2d..31fa64ab 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -16,30 +16,15 @@ using Penumbra.String.Classes; namespace Penumbra.Mods; -public partial class ModCreator +public partial class ModCreator(SaveService _saveService, Configuration _config, ModDataEditor _dataEditor, MetaFileManager _metaFileManager, + IGamePathParser _gamePathParser) { - private readonly Configuration _config; - private readonly SaveService _saveService; - private readonly ModDataEditor _dataEditor; - private readonly MetaFileManager _metaFileManager; - private readonly IGamePathParser _gamePathParser; - - public ModCreator(SaveService saveService, Configuration config, ModDataEditor dataEditor, MetaFileManager metaFileManager, - IGamePathParser gamePathParser) - { - _saveService = saveService; - _config = config; - _dataEditor = dataEditor; - _metaFileManager = metaFileManager; - _gamePathParser = gamePathParser; - } - /// Creates directory and files necessary for a new mod without adding it to the manager. public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "") { try { - var newDir = CreateModFolder(basePath, newName); + var newDir = CreateModFolder(basePath, newName, _config.ReplaceNonAsciiOnImport, true); _dataEditor.CreateMeta(newDir, newName, _config.DefaultModAuthor, description, "1.0", string.Empty); CreateDefaultFiles(newDir); return newDir; @@ -138,13 +123,13 @@ public partial class ModCreator /// - Unique, by appending (digit) for duplicates.
/// - Containing no symbols invalid for FFXIV or windows paths.
/// - public static DirectoryInfo CreateModFolder(DirectoryInfo outDirectory, string modListName, bool create = true) + public static DirectoryInfo CreateModFolder(DirectoryInfo outDirectory, string modListName, bool onlyAscii, bool create) { var name = modListName; if (name.Length == 0) name = "_"; - var newModFolderBase = NewOptionDirectory(outDirectory, name); + var newModFolderBase = NewOptionDirectory(outDirectory, name, onlyAscii); var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); if (newModFolder.Length == 0) throw new IOException("Could not create mod folder: too many folders of the same name exist."); @@ -238,9 +223,9 @@ public partial class ModCreator /// Create the name for a group or option subfolder based on its parent folder and given name. /// subFolderName should never be empty, and the result is unique and contains no invalid symbols. /// - public static DirectoryInfo? NewSubFolderName(DirectoryInfo parentFolder, string subFolderName) + public static DirectoryInfo? NewSubFolderName(DirectoryInfo parentFolder, string subFolderName, bool onlyAscii) { - var newModFolderBase = NewOptionDirectory(parentFolder, subFolderName); + var newModFolderBase = NewOptionDirectory(parentFolder, subFolderName, onlyAscii); var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); return newModFolder.Length == 0 ? null : new DirectoryInfo(newModFolder); } @@ -325,14 +310,14 @@ public partial class ModCreator } /// Return the name of a new valid directory based on the base directory and the given name. - public static DirectoryInfo NewOptionDirectory(DirectoryInfo baseDir, string optionName) + public static DirectoryInfo NewOptionDirectory(DirectoryInfo baseDir, string optionName, bool onlyAscii) { - var option = ReplaceBadXivSymbols(optionName); + var option = ReplaceBadXivSymbols(optionName, onlyAscii); return new DirectoryInfo(Path.Combine(baseDir.FullName, option.Length > 0 ? option : "_")); } /// Normalize for nicer names, and remove invalid symbols or invalid paths. - public static string ReplaceBadXivSymbols(string s, string replacement = "_") + public static string ReplaceBadXivSymbols(string s, bool onlyAscii, string replacement = "_") { switch (s) { @@ -345,6 +330,8 @@ public partial class ModCreator { if (c.IsInvalidInPath()) sb.Append(replacement); + else if (onlyAscii && c.IsInvalidAscii()) + sb.Append(replacement); else sb.Append(c); } diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 73273707..dc73b451 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -52,7 +52,7 @@ public class TemporaryMod : IMod DirectoryInfo? dir = null; try { - dir = ModCreator.CreateModFolder(modManager.BasePath, collection.Name); + dir = ModCreator.CreateModFolder(modManager.BasePath, collection.Name, config.ReplaceNonAsciiOnImport, true); var fileDir = Directory.CreateDirectory(Path.Combine(dir.FullName, "files")); modManager.DataEditor.CreateMeta(dir, collection.Name, character ?? config.DefaultModAuthor, $"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null); diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 8597bc0c..5347208e 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -25,6 +25,7 @@ namespace Penumbra.UI.AdvancedWindow; public class ItemSwapTab : IDisposable, ITab { + private readonly Configuration _config; private readonly CommunicatorService _communicator; private readonly ItemService _itemService; private readonly CollectionManager _collectionManager; @@ -32,13 +33,14 @@ public class ItemSwapTab : IDisposable, ITab private readonly MetaFileManager _metaFileManager; public ItemSwapTab(CommunicatorService communicator, ItemService itemService, CollectionManager collectionManager, - ModManager modManager, IdentifierService identifier, MetaFileManager metaFileManager) + ModManager modManager, IdentifierService identifier, MetaFileManager metaFileManager, Configuration config) { _communicator = communicator; _itemService = itemService; _collectionManager = collectionManager; _modManager = modManager; _metaFileManager = metaFileManager; + _config = config; _swapData = new ItemSwapContainer(metaFileManager, identifier); _selectors = new Dictionary @@ -296,7 +298,7 @@ public class ItemSwapTab : IDisposable, ITab { optionFolderName = ModCreator.NewSubFolderName(new DirectoryInfo(Path.Combine(_mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName)), - _newOptionName); + _newOptionName, _config.ReplaceNonAsciiOnImport); if (optionFolderName?.Exists == true) throw new Exception($"The folder {optionFolderName.FullName} for the option already exists."); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 63ea8581..64457c25 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -22,12 +22,13 @@ public partial class ModEditWindow private HashSet GetPlayerResourcesOfType(ResourceType type) { - var resources = ResourceTreeApiHelper.GetResourcesOfType(_resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly), type) + var resources = ResourceTreeApiHelper + .GetResourcesOfType(_resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly), type) .Values .SelectMany(resources => resources.Values) .Select(resource => resource.Item1); - return new(resources, StringComparer.OrdinalIgnoreCase); + return new HashSet(resources, StringComparer.OrdinalIgnoreCase); } private IReadOnlyList PopulateIsOnPlayer(IReadOnlyList files, ResourceType type) @@ -198,7 +199,7 @@ public partial class ModEditWindow if (mod == null) return new QuickImportAction(editor, optionName, gamePath); - var (preferredPath, subDirs) = GetPreferredPath(mod, subMod); + var (preferredPath, subDirs) = GetPreferredPath(mod, subMod, owner._config.ReplaceNonAsciiOnImport); var targetPath = new FullPath(Path.Combine(preferredPath.FullName, gamePath.ToString())).FullName; if (File.Exists(targetPath)) return new QuickImportAction(editor, optionName, gamePath); @@ -226,7 +227,7 @@ public partial class ModEditWindow return fileRegistry; } - private static (DirectoryInfo, int) GetPreferredPath(Mod mod, ISubMod subMod) + private static (DirectoryInfo, int) GetPreferredPath(Mod mod, ISubMod subMod, bool replaceNonAscii) { var path = mod.ModPath; var subDirs = 0; @@ -237,13 +238,13 @@ public partial class ModEditWindow var fullName = subMod.FullName; if (fullName.EndsWith(": " + name)) { - path = ModCreator.NewOptionDirectory(path, fullName[..^(name.Length + 2)]); - path = ModCreator.NewOptionDirectory(path, name); + path = ModCreator.NewOptionDirectory(path, fullName[..^(name.Length + 2)], replaceNonAscii); + path = ModCreator.NewOptionDirectory(path, name, replaceNonAscii); subDirs = 2; } else { - path = ModCreator.NewOptionDirectory(path, fullName); + path = ModCreator.NewOptionDirectory(path, fullName, replaceNonAscii); subDirs = 1; } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 70a94ecd..104f8d91 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -535,6 +535,9 @@ public class SettingsTab : ITab /// Draw all settings pertaining to import and export of mods. private void DrawModHandlingSettings() { + Checkbox("Replace Non-Standard Symbols On Import", + "Replace all non-ASCII symbols in mod and option names with underscores when importing mods.", _config.ReplaceNonAsciiOnImport, + v => _config.ReplaceNonAsciiOnImport = v); Checkbox("Always Open Import at Default Directory", "Open the import window at the location specified here every time, forgetting your previous path.", _config.AlwaysOpenDefaultImport, v => _config.AlwaysOpenDefaultImport = v);