Implement start of new file system and saving, update a lot of things to ImSharp.

This commit is contained in:
Ottermandias 2025-10-27 11:07:58 +01:00
parent 6b475ee229
commit c098fbdfe8
71 changed files with 1115 additions and 986 deletions

View file

@ -7,25 +7,27 @@ using Penumbra.Services;
namespace Penumbra.Mods.Manager;
[Flags]
public enum ModDataChangeType : ushort
public enum ModDataChangeType : uint
{
None = 0x0000,
Name = 0x0001,
Author = 0x0002,
Description = 0x0004,
Version = 0x0008,
Website = 0x0010,
Deletion = 0x0020,
Migration = 0x0040,
ModTags = 0x0080,
ImportDate = 0x0100,
Favorite = 0x0200,
LocalTags = 0x0400,
Note = 0x0800,
Image = 0x1000,
DefaultChangedItems = 0x2000,
PreferredChangedItems = 0x4000,
RequiredFeatures = 0x8000,
None = 0x000000,
Name = 0x000001,
Author = 0x000002,
Description = 0x000004,
Version = 0x000008,
Website = 0x000010,
Deletion = 0x000020,
Migration = 0x000040,
ModTags = 0x000080,
ImportDate = 0x000100,
Favorite = 0x000200,
LocalTags = 0x000400,
Note = 0x000800,
Image = 0x001000,
DefaultChangedItems = 0x002000,
PreferredChangedItems = 0x004000,
RequiredFeatures = 0x008000,
FileSystemFolder = 0x010000,
FileSystemSortOrder = 0x020000,
}
public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService, ItemData itemData) : Luna.IService

View file

@ -7,102 +7,73 @@ using FileSystemChangeType = OtterGui.Filesystem.FileSystemChangeType;
namespace Penumbra.Mods.Manager;
//public sealed class ModFileSystem2 : BaseFileSystem
//{
// private readonly Configuration _config;
// private readonly SaveService _saveService;
// public ModFileSystem2(FileSystemChanged @event, DataNodePathChange dataChangeEvent, Configuration config, SaveService saveService, IComparer<ReadOnlySpan<char>>? comparer = null)
// : base(@event, dataChangeEvent, comparer)
// {
// _config = config;
// _saveService = saveService;
// }
//
// public void Dispose()
// {
// _communicator.ModPathChanged.Unsubscribe(OnModPathChange);
// _communicator.ModDiscoveryFinished.Unsubscribe(Reload);
// _communicator.ModDataChanged.Unsubscribe(OnModDataChange);
// }
//
// // Save the filesystem on every filesystem change except full reloading.
// private void OnChange(FileSystemChangeType type, IPath _1, IPath? _2, IPath? _3)
// {
// if (type != FileSystemChangeType.Reload)
// _saveService.DelaySave(this);
// }
//
// // Update sort order when defaulted mod names change.
// private void OnModDataChange(in ModDataChanged.Arguments arguments)
// {
// if (!arguments.Type.HasFlag(ModDataChangeType.Name) || arguments.OldName == null || !TryGetValue(arguments.Mod, out var leaf))
// return;
//
// var old = Extensions.FixName(arguments.OldName);
// if (old == leaf.Name || Extensions.IsDuplicateName(leaf.Name, out var baseName, out _) && baseName == old)
// RenameWithDuplicates(leaf, arguments.Mod.Name);
// }
//
// // Update the filesystem if a mod has been added or removed.
// // Save it, if the mod directory has been moved, since this will change the save format.
// private void OnModPathChange(in ModPathChanged.Arguments arguments)
// {
// switch (arguments.Type)
// {
// case ModPathChangeType.Added:
// var parent = Root;
// if (_config.DefaultImportFolder.Length != 0)
// try
// {
// parent = FindOrCreateAllFolders(_config.DefaultImportFolder);
// }
// catch (Exception e)
// {
// Penumbra.Messager.NotificationMessage(e,
// $"Could not move newly imported mod {arguments.Mod.Name} to default import folder {_config.DefaultImportFolder}.",
// NotificationType.Warning);
// }
//
// CreateDuplicateLeaf(parent, arguments.Mod.Name, arguments.Mod);
// break;
// case ModPathChangeType.Deleted:
// if (arguments.Mod.Node is not null)
// Delete(arguments.Mod.Node);
// break;
// case ModPathChangeType.Moved:
// _saveService.DelaySave(this);
// break;
// case ModPathChangeType.Reloaded:
// // Nothing
// break;
// }
// }
//
// public struct ImportDate : ISortMode
// {
// public ReadOnlySpan<byte> Name
// => "Import Date (Older First)"u8;
//
// public ReadOnlySpan<byte> Description
// => "In each folder, sort all subfolders lexicographically, then sort all leaves using their import date."u8;
//
// public IEnumerable<IFileSystemNode> GetChildren(IFileSystemFolder f)
// => f.GetSubFolders().Cast<IFileSystemNode>().Concat(f.GetLeaves().OfType<IFileSystemData<Mod>>().OrderBy(l => l.Value.ImportDate));
// }
//
// public struct InverseImportDate : ISortMode
// {
// public ReadOnlySpan<byte> Name
// => "Import Date (Newer First)"u8;
//
// public ReadOnlySpan<byte> Description
// => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse import date."u8;
//
// public IEnumerable<IFileSystemNode> GetChildren(IFileSystemFolder f)
// => f.GetSubFolders().Cast<IFileSystemNode>().Concat(f.GetLeaves().OfType<IFileSystemData<Mod>>().OrderByDescending(l => l.Value.ImportDate));
// }
//
//}
public sealed class ModFileSystem2 : BaseFileSystem, IDisposable, IRequiredService
{
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
private readonly ModFileSystemSaver _saver;
public ModFileSystem2(Configuration config, CommunicatorService communicator, SaveService saveService, Logger log, ModStorage modStorage)
: base("ModFileSystem", log)
{
_config = config;
_communicator = communicator;
_saver = new ModFileSystemSaver(log, this, saveService, modStorage);
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModFileSystem);
_communicator.ModDiscoveryFinished.Subscribe(_saver.Load, ModDiscoveryFinished.Priority.ModFileSystem);
_communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModFileSystem);
_saver.Load();
}
public void Dispose()
{
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
_communicator.ModDiscoveryFinished.Unsubscribe(_saver.Load);
_communicator.ModDataChanged.Unsubscribe(OnModDataChange);
}
// Update sort order when defaulted mod names change.
private void OnModDataChange(in ModDataChanged.Arguments arguments)
{
if (arguments.Type.HasFlag(ModDataChangeType.FileSystemFolder))
RenameAndMoveWithDuplicates(arguments.Mod.Node!, arguments.Mod.Path.GetIntendedPath(arguments.Mod.Name));
else if (arguments.Type.HasFlag(ModDataChangeType.Name) && arguments.Mod.Path.SortName is null
|| arguments.Type.HasFlag(ModDataChangeType.FileSystemSortOrder))
RenameWithDuplicates(arguments.Mod.Node!, arguments.Mod.Path.GetIntendedName(arguments.Mod.Name));
}
// Update the filesystem if a mod has been added or removed.
// Save it, if the mod directory has been moved, since this will change the save format.
private void OnModPathChange(in ModPathChanged.Arguments arguments)
{
switch (arguments.Type)
{
case ModPathChangeType.Added:
var parent = Root;
if (_config.DefaultImportFolder.Length is not 0)
try
{
parent = FindOrCreateAllFolders(_config.DefaultImportFolder);
}
catch (Exception e)
{
Penumbra.Messager.NotificationMessage(e,
$"Could not move newly imported mod {arguments.Mod.Name} to default import folder {_config.DefaultImportFolder}.",
NotificationType.Warning);
}
CreateDuplicateDataNode(parent, arguments.Mod.Name, arguments.Mod);
break;
case ModPathChangeType.Deleted:
if (arguments.Mod.Node is not null)
Delete(arguments.Mod.Node);
break;
case ModPathChangeType.Reloaded:
// Nothing
break;
}
}
}
public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable, IService
{
@ -160,7 +131,7 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable, ISer
// Used on construction and on mod rediscoveries.
private void Reload()
{
var jObj = BackupService.GetJObjectForFile(_saveService.FileNames, _saveService.FileNames.FilesystemFile);
var jObj = BackupService.GetJObjectForFile(_saveService.FileNames, _saveService.FileNames.OldFilesystemFile);
if (Load(jObj, _modManager, ModToIdentifier, ModToName))
_saveService.ImmediateSave(this);
@ -212,9 +183,7 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable, ISer
Delete(leaf);
break;
case ModPathChangeType.Moved:
_saveService.DelaySave(this);
break;
case ModPathChangeType.Moved: _saveService.DelaySave(this); break;
case ModPathChangeType.Reloaded:
// Nothing
break;
@ -232,7 +201,7 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable, ISer
public static bool ModHasDefaultPath(Mod mod, string fullPath)
{
var regex = new Regex($@"^{Regex.Escape(ModToName(mod))}( \(\d+\))?$");
return regex.IsMatch(fullPath);
return regex.IsMatch(fullPath);
}
private static (string, bool) SaveMod(Mod mod, string fullPath)
@ -242,7 +211,7 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable, ISer
: (ModToIdentifier(mod), true);
public string ToFilePath(FilenameService fileNames)
=> fileNames.FilesystemFile;
=> fileNames.OldFilesystemFile;
public void Save(StreamWriter writer)
=> SaveToFile(writer, SaveMod, true);

View file

@ -0,0 +1,54 @@
using Luna;
using Penumbra.Services;
namespace Penumbra.Mods.Manager;
public sealed class ModFileSystemSaver(Logger log, BaseFileSystem fileSystem, SaveService saveService, ModStorage mods)
: FileSystemSaver<SaveService, FilenameService>(log, fileSystem, saveService)
{
protected override string LockedFile(FilenameService provider)
=> provider.FileSystemLockedNodes;
protected override string ExpandedFile(FilenameService provider)
=> provider.FileSystemExpandedFolders;
protected override string EmptyFoldersFile(FilenameService provider)
=> provider.FileSystemEmptyFolders;
protected override string MigrationFile(FilenameService provider)
=> provider.OldFilesystemFile;
protected override bool GetValueFromIdentifier(ReadOnlySpan<char> identifier, [NotNullWhen(true)] out IFileSystemValue? value)
{
if (mods.TryGetMod(identifier, out var mod))
{
value = mod;
return true;
}
value = null;
return false;
}
protected override void CreateDataNodes()
{
foreach (var mod in mods)
{
try
{
var folder = mod.Path.Folder.Length is 0 ? FileSystem.Root : FileSystem.FindOrCreateAllFolders(mod.Path.Folder);
FileSystem.CreateDuplicateDataNode(folder, mod.Path.SortName ?? mod.Name, mod);
}
catch (Exception e)
{
Log.Error($"Could not create folder structure for mod {mod.Name} at path {mod.Path.Folder}: {e}");
}
}
}
protected override void SaveDataValue(IFileSystemValue value)
{
if (value is Mod mod)
SaveService.QueueSave(new ModLocalData(mod));
}
}

View file

@ -20,20 +20,27 @@ public class ModStorage : IReadOnlyList<Mod>
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/// <summary>
/// Try to obtain a mod by its directory name (unique identifier).
/// </summary>
public bool TryGetMod(string identifier, [NotNullWhen(true)] out Mod? mod)
/// <summary> Try to obtain a mod by its directory name (unique identifier). </summary>
public bool TryGetMod(ReadOnlySpan<char> identifier, [NotNullWhen(true)] out Mod? mod)
{
mod = this.FirstOrDefault(m => string.Equals(m.Identifier, identifier, StringComparison.OrdinalIgnoreCase));
return mod is not null;
foreach (var m in Mods)
{
if (!identifier.Equals(m.Identifier, StringComparison.OrdinalIgnoreCase))
continue;
mod = m;
return true;
}
mod = null;
return false;
}
/// <summary>
/// Try to obtain a mod by its directory name (unique identifier, preferred),
/// or the first mod of the given name if no directory fits.
/// </summary>
public bool TryGetMod(string identifier, string modName, [NotNullWhen(true)] out Mod? mod)
public bool TryGetMod(ReadOnlySpan<char> identifier, ReadOnlySpan<char> modName, [NotNullWhen(true)] out Mod? mod)
{
if (modName.Length is 0)
return TryGetMod(identifier, out mod);
@ -41,13 +48,13 @@ public class ModStorage : IReadOnlyList<Mod>
mod = null;
foreach (var m in Mods)
{
if (string.Equals(m.Identifier, identifier, StringComparison.OrdinalIgnoreCase))
if (identifier.Equals(m.Identifier, StringComparison.OrdinalIgnoreCase))
{
mod = m;
return true;
}
if (m.Name == modName)
if (m.Name.SequenceEqual(modName))
mod ??= m;
}

View file

@ -0,0 +1,28 @@
using Luna;
namespace Penumbra.Mods.Manager;
public readonly struct ImportDate : ISortMode
{
public ReadOnlySpan<byte> Name
=> "Import Date (Older First)"u8;
public ReadOnlySpan<byte> Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their import date."u8;
public IEnumerable<IFileSystemNode> GetChildren(IFileSystemFolder f)
=> f.GetSubFolders().Cast<IFileSystemNode>().Concat(f.GetLeaves().OfType<IFileSystemData<Mod>>().OrderBy(l => l.Value.ImportDate));
}
public readonly struct InverseImportDate : ISortMode
{
public ReadOnlySpan<byte> Name
=> "Import Date (Newer First)"u8;
public ReadOnlySpan<byte> Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse import date."u8;
public IEnumerable<IFileSystemNode> GetChildren(IFileSystemFolder f)
=> f.GetSubFolders().Cast<IFileSystemNode>()
.Concat(f.GetLeaves().OfType<IFileSystemData<Mod>>().OrderByDescending(l => l.Value.ImportDate));
}

View file

@ -69,7 +69,7 @@ public sealed class Mod : IMod, IFileSystemValue<Mod>
// Local Data
public string FullPath { get; set; } = string.Empty;
public DataPath Path { get; } = new();
public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds();
public IReadOnlyList<string> LocalTags { get; internal set; } = [];
public string Note { get; internal set; } = string.Empty;
@ -137,9 +137,12 @@ public sealed class Mod : IMod, IFileSystemValue<Mod>
public string LowerChangedItemsString { get; internal set; } = string.Empty;
public string AllTagsLower { get; internal set; } = string.Empty;
public int TotalFileCount { get; internal set; }
public int TotalSwapCount { get; internal set; }
public int TotalManipulations { get; internal set; }
public ushort LastChangedItemsUpdate { get; internal set; }
public bool HasOptions { get; internal set; }
public int TotalFileCount { get; internal set; }
public int TotalSwapCount { get; internal set; }
public int TotalManipulations { get; internal set; }
public ushort LastChangedItemsUpdate { get; internal set; }
public bool HasOptions { get; internal set; }
string IFileSystemValue.DisplayName
=> Name;
}

View file

@ -26,11 +26,10 @@ public readonly struct ModLocalData(Mod mod) : ISavable
{ nameof(Mod.PreferredChangedItems), JToken.FromObject(mod.PreferredChangedItems) },
};
if (mod.FullPath.Length > 0)
{
var baseName = mod.FullPath.GetBaseName(mod.Name, out var folder);
jObject[nameof(Mod.FullPath)] = folder.Length > 0 ? $"{folder}/{baseName}" : baseName.ToString();
}
if (mod.Path.Folder.Length > 0)
jObject["FileSystemFolder"] = mod.Path.Folder;
if (mod.Path.SortName is not null)
jObject["SortOrderName"] = mod.Path.SortName;
using var jWriter = new JsonTextWriter(writer);
jWriter.Formatting = Formatting.Indented;
@ -41,10 +40,12 @@ public readonly struct ModLocalData(Mod mod) : ISavable
{
var dataFile = editor.SaveService.FileNames.LocalDataFile(mod);
var importDate = 0L;
var localTags = Enumerable.Empty<string>();
var favorite = false;
var note = string.Empty;
var importDate = 0L;
var localTags = Enumerable.Empty<string>();
var favorite = false;
var note = string.Empty;
var fileSystemFolder = string.Empty;
string? sortOrderName = null;
HashSet<CustomItemId> preferredChangedItems = [];
@ -62,7 +63,9 @@ public readonly struct ModLocalData(Mod mod) : ISavable
preferredChangedItems =
(json[nameof(Mod.PreferredChangedItems)] as JArray)?.Values<ulong>().Select(i => (CustomItemId)i).ToHashSet()
?? mod.DefaultPreferredItems;
save = false;
fileSystemFolder = json["FileSystemFolder"]?.Value<string>() ?? string.Empty;
sortOrderName = json["SortOrderName"]?.Value<string>()?.FixName();
save = false;
}
catch (Exception e)
{
@ -101,6 +104,18 @@ public readonly struct ModLocalData(Mod mod) : ISavable
changes |= ModDataChangeType.PreferredChangedItems;
}
if (!mod.Path.Folder.Equals(fileSystemFolder, StringComparison.OrdinalIgnoreCase))
{
mod.Path.Folder = fileSystemFolder;
changes |= ModDataChangeType.FileSystemFolder;
}
if (mod.Path.SortName != sortOrderName)
{
mod.Path.SortName = sortOrderName;
changes |= ModDataChangeType.FileSystemSortOrder;
}
if (save)
editor.SaveService.QueueSave(new ModLocalData(mod));