Start ModManager dissemination....

This commit is contained in:
Ottermandias 2023-03-24 00:28:36 +01:00
parent 174e640c45
commit c8415e3079
34 changed files with 1305 additions and 1542 deletions

File diff suppressed because it is too large Load diff

View file

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Api.Helpers; using Penumbra.Api.Helpers;
using Penumbra.Mods;
namespace Penumbra.Api; namespace Penumbra.Api;
@ -111,7 +112,7 @@ public class PenumbraIpcProviders : IDisposable
internal readonly FuncProvider< string, int, PenumbraApiEc > RemoveTemporaryModAll; internal readonly FuncProvider< string, int, PenumbraApiEc > RemoveTemporaryModAll;
internal readonly FuncProvider< string, string, int, PenumbraApiEc > RemoveTemporaryMod; internal readonly FuncProvider< string, string, int, PenumbraApiEc > RemoveTemporaryMod;
public PenumbraIpcProviders( DalamudPluginInterface pi, IPenumbraApi api ) public PenumbraIpcProviders( DalamudPluginInterface pi, IPenumbraApi api, Mod.Manager modManager )
{ {
Api = api; Api = api;
@ -219,7 +220,7 @@ public class PenumbraIpcProviders : IDisposable
RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider( pi, Api.RemoveTemporaryModAll ); RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider( pi, Api.RemoveTemporaryModAll );
RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider( pi, Api.RemoveTemporaryMod ); RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider( pi, Api.RemoveTemporaryMod );
Tester = new IpcTester( pi, this ); Tester = new IpcTester( pi, this, modManager );
Initialized.Invoke(); Initialized.Invoke();
} }

View file

@ -17,7 +17,7 @@ namespace Penumbra.Collections;
public partial class ModCollection public partial class ModCollection
{ {
public sealed partial class Manager : ISaveable public sealed partial class Manager : ISavable
{ {
public const int Version = 1; public const int Version = 1;

View file

@ -11,7 +11,7 @@ using Penumbra.Util;
namespace Penumbra.Collections; namespace Penumbra.Collections;
// File operations like saving, loading and deleting for a collection. // File operations like saving, loading and deleting for a collection.
public partial class ModCollection : ISaveable public partial class ModCollection : ISavable
{ {
// Since inheritances depend on other collections existing, // Since inheritances depend on other collections existing,
// we return them as a list to be applied after reading all collections. // we return them as a list to be applied after reading all collections.

View file

@ -9,23 +9,21 @@ using OtterGui.Classes;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.Import.Structs; using Penumbra.Import.Structs;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI; using Penumbra.UI;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
using Penumbra.Util;
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
namespace Penumbra; namespace Penumbra;
[Serializable] [Serializable]
public class Configuration : IPluginConfiguration public class Configuration : IPluginConfiguration, ISavable
{ {
[JsonIgnore] [JsonIgnore]
private readonly string _fileName; private readonly SaveService _saveService;
[JsonIgnore]
private readonly FrameworkManager _framework;
public int Version { get; set; } = Constants.CurrentVersion; public int Version { get; set; } = Constants.CurrentVersion;
@ -101,14 +99,13 @@ public class Configuration : IPluginConfiguration
/// Load the current configuration. /// Load the current configuration.
/// Includes adding new colors and migrating from old versions. /// Includes adding new colors and migrating from old versions.
/// </summary> /// </summary>
public Configuration(FilenameService fileNames, ConfigMigrationService migrator, FrameworkManager framework) public Configuration(FilenameService fileNames, ConfigMigrationService migrator, SaveService saveService)
{ {
_fileName = fileNames.ConfigFile; _saveService = saveService;
_framework = framework; Load(fileNames, migrator);
Load(migrator);
} }
public void Load(ConfigMigrationService migrator) public void Load(FilenameService fileNames, ConfigMigrationService migrator)
{ {
static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs)
{ {
@ -117,9 +114,9 @@ public class Configuration : IPluginConfiguration
errorArgs.ErrorContext.Handled = true; errorArgs.ErrorContext.Handled = true;
} }
if (File.Exists(_fileName)) if (File.Exists(fileNames.ConfigFile))
{ {
var text = File.ReadAllText(_fileName); var text = File.ReadAllText(fileNames.ConfigFile);
JsonConvert.PopulateObject(text, this, new JsonSerializerSettings JsonConvert.PopulateObject(text, this, new JsonSerializerSettings
{ {
Error = HandleDeserializationError, Error = HandleDeserializationError,
@ -130,21 +127,8 @@ public class Configuration : IPluginConfiguration
} }
/// <summary> Save the current configuration. </summary> /// <summary> Save the current configuration. </summary>
private void SaveConfiguration()
{
try
{
var text = JsonConvert.SerializeObject(this, Formatting.Indented);
File.WriteAllText(_fileName, text);
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not save plugin configuration:\n{e}");
}
}
public void Save() public void Save()
=> _framework.RegisterDelayed(nameof(SaveConfiguration), SaveConfiguration); => _saveService.QueueSave(this);
/// <summary> Contains some default values or boundaries for config values. </summary> /// <summary> Contains some default values or boundaries for config values. </summary>
public static class Constants public static class Constants
@ -192,4 +176,14 @@ public class Configuration : IPluginConfiguration
return mode; return mode;
} }
} }
public string ToFilename(FilenameService fileNames)
=> fileNames.ConfigFile;
public void Save(StreamWriter writer)
{
using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
serializer.Serialize(jWriter, this);
}
} }

View file

@ -7,6 +7,7 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
using Penumbra.Api;
using Penumbra.Import.Structs; using Penumbra.Import.Structs;
using Penumbra.Mods; using Penumbra.Mods;
using FileMode = System.IO.FileMode; using FileMode = System.IO.FileMode;
@ -33,22 +34,19 @@ public partial class TexToolsImporter : IDisposable
public ImporterState State { get; private set; } public ImporterState State { get; private set; }
public readonly List< (FileInfo File, DirectoryInfo? Mod, Exception? Error) > ExtractedMods; public readonly List< (FileInfo File, DirectoryInfo? Mod, Exception? Error) > ExtractedMods;
public TexToolsImporter( DirectoryInfo baseDirectory, ICollection< FileInfo > files,
Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor)
: this( baseDirectory, files.Count, files, handler, config, editor)
{ }
private readonly Configuration _config; private readonly Configuration _config;
private readonly ModEditor _editor; private readonly ModEditor _editor;
private readonly Mod.Manager _modManager;
public TexToolsImporter( DirectoryInfo baseDirectory, int count, IEnumerable< FileInfo > modPackFiles, public TexToolsImporter( DirectoryInfo baseDirectory, int count, IEnumerable< FileInfo > modPackFiles,
Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor) Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor, Mod.Manager modManager)
{ {
_baseDirectory = baseDirectory; _baseDirectory = baseDirectory;
_tmpFile = Path.Combine( _baseDirectory.FullName, TempFileName ); _tmpFile = Path.Combine( _baseDirectory.FullName, TempFileName );
_modPackFiles = modPackFiles; _modPackFiles = modPackFiles;
_config = config; _config = config;
_editor = editor; _editor = editor;
_modManager = modManager;
_modPackCount = count; _modPackCount = count;
ExtractedMods = new List< (FileInfo, DirectoryInfo?, Exception?) >( count ); ExtractedMods = new List< (FileInfo, DirectoryInfo?, Exception?) >( count );
_token = _cancellation.Token; _token = _cancellation.Token;

View file

@ -35,7 +35,7 @@ public partial class TexToolsImporter
_currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) ); _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) );
// Create a new ModMeta from the TTMP mod list info // Create a new ModMeta from the TTMP mod list info
Mod.Creator.CreateMeta( _currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null ); _modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null );
// Open the mod data file from the mod pack as a SqPackStream // Open the mod data file from the mod pack as a SqPackStream
_streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); _streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" );
@ -90,7 +90,7 @@ public partial class TexToolsImporter
Penumbra.Log.Information( " -> Importing Simple V2 ModPack" ); Penumbra.Log.Information( " -> Importing Simple V2 ModPack" );
_currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, _currentModName ); _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, _currentModName );
Mod.Creator.CreateMeta( _currentModDirectory, _currentModName, modList.Author, string.IsNullOrEmpty( modList.Description ) _modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, modList.Author, string.IsNullOrEmpty( modList.Description )
? "Mod imported from TexTools mod pack" ? "Mod imported from TexTools mod pack"
: modList.Description, modList.Version, modList.Url ); : modList.Description, modList.Version, modList.Url );
@ -135,7 +135,7 @@ public partial class TexToolsImporter
_currentModName = modList.Name; _currentModName = modList.Name;
_currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, _currentModName ); _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, _currentModName );
Mod.Creator.CreateMeta( _currentModDirectory, _currentModName, modList.Author, modList.Description, modList.Version, modList.Url ); _modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, modList.Author, modList.Description, modList.Version, modList.Url );
if( _currentNumOptions == 0 ) if( _currentNumOptions == 0 )
{ {

View file

@ -243,7 +243,7 @@ public class DuplicateManager
try try
{ {
var mod = new Mod(modDirectory); var mod = new Mod(modDirectory);
mod.Reload(true, out _); mod.Reload(_modManager, true, out _);
Finished = false; Finished = false;
_files.UpdateAll(mod, mod.Default); _files.UpdateAll(mod, mod.Default);

View file

@ -154,7 +154,7 @@ public class ModFileEditor
if (deletions <= 0) if (deletions <= 0)
return; return;
mod.Reload(false, out _); mod.Reload(_modManager, false, out _);
_files.UpdateAll(mod, option); _files.UpdateAll(mod, option);
} }

View file

@ -8,20 +8,20 @@ public partial class Mod
{ {
public partial class Manager public partial class Manager
{ {
public delegate void ModPathChangeDelegate( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, public delegate void ModPathChangeDelegate(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
DirectoryInfo? newDirectory ); DirectoryInfo? newDirectory);
public event ModPathChangeDelegate ModPathChanged; public event ModPathChangeDelegate ModPathChanged;
// Rename/Move a mod directory. // Rename/Move a mod directory.
// Updates all collection settings and sort order settings. // Updates all collection settings and sort order settings.
public void MoveModDirectory( int idx, string newName ) public void MoveModDirectory(int idx, string newName)
{ {
var mod = this[ idx ]; var mod = this[idx];
var oldName = mod.Name; var oldName = mod.Name;
var oldDirectory = mod.ModPath; var oldDirectory = mod.ModPath;
switch( NewDirectoryValid( oldDirectory.Name, newName, out var dir ) ) switch (NewDirectoryValid(oldDirectory.Name, newName, out var dir))
{ {
case NewDirectoryState.NonExisting: case NewDirectoryState.NonExisting:
// Nothing to do // Nothing to do
@ -29,11 +29,11 @@ public partial class Mod
case NewDirectoryState.ExistsEmpty: case NewDirectoryState.ExistsEmpty:
try try
{ {
Directory.Delete( dir!.FullName ); Directory.Delete(dir!.FullName);
} }
catch( Exception e ) catch (Exception e)
{ {
Penumbra.Log.Error( $"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}" ); Penumbra.Log.Error($"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}");
return; return;
} }
@ -50,105 +50,97 @@ public partial class Mod
try try
{ {
Directory.Move( oldDirectory.FullName, dir!.FullName ); Directory.Move(oldDirectory.FullName, dir!.FullName);
} }
catch( Exception e ) catch (Exception e)
{ {
Penumbra.Log.Error( $"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}" ); Penumbra.Log.Error($"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}");
return; return;
} }
MoveDataFile( oldDirectory, dir ); DataEditor.MoveDataFile(oldDirectory, dir);
new ModBackup( this, mod ).Move( null, dir.Name ); new ModBackup(this, mod).Move(null, dir.Name);
dir.Refresh(); dir.Refresh();
mod.ModPath = dir; mod.ModPath = dir;
if( !mod.Reload( false, out var metaChange ) ) if (!mod.Reload(this, false, out var metaChange))
{ {
Penumbra.Log.Error( $"Error reloading moved mod {mod.Name}." ); Penumbra.Log.Error($"Error reloading moved mod {mod.Name}.");
return; return;
} }
ModPathChanged.Invoke( ModPathChangeType.Moved, mod, oldDirectory, dir ); ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir);
if( metaChange != ModDataChangeType.None ) if (metaChange != ModDataChangeType.None)
{ _communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
ModDataChanged?.Invoke( metaChange, mod, oldName );
}
} }
// Reload a mod without changing its base directory. /// <summary>
// If the base directory does not exist anymore, the mod will be deleted. /// Reload a mod without changing its base directory.
public void ReloadMod( int idx ) /// If the base directory does not exist anymore, the mod will be deleted.
/// </summary>
public void ReloadMod(int idx)
{ {
var mod = this[ idx ]; var mod = this[idx];
var oldName = mod.Name; var oldName = mod.Name;
ModPathChanged.Invoke( ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath ); ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath);
if( !mod.Reload( true, out var metaChange ) ) if (!mod.Reload(this, true, out var metaChange))
{ {
Penumbra.Log.Warning( mod.Name.Length == 0 Penumbra.Log.Warning(mod.Name.Length == 0
? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead." ? $"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." ); : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead.");
DeleteMod( idx ); DeleteMod(idx);
return; return;
} }
ModPathChanged.Invoke( ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath ); ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath);
if( metaChange != ModDataChangeType.None ) if (metaChange != ModDataChangeType.None)
{ _communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
ModDataChanged?.Invoke( metaChange, mod, oldName );
}
} }
// Delete a mod by its index. The event is invoked before the mod is removed from the list. /// <summary>
// Deletes from filesystem as well as from internal data. /// Delete a mod by its index. The event is invoked before the mod is removed from the list.
// Updates indices of later mods. /// Deletes from filesystem as well as from internal data.
public void DeleteMod( int idx ) /// Updates indices of later mods.
/// </summary>
public void DeleteMod(int idx)
{ {
var mod = this[ idx ]; var mod = this[idx];
if( Directory.Exists( mod.ModPath.FullName ) ) if (Directory.Exists(mod.ModPath.FullName))
{
try try
{ {
Directory.Delete( mod.ModPath.FullName, true ); Directory.Delete(mod.ModPath.FullName, true);
Penumbra.Log.Debug( $"Deleted directory {mod.ModPath.FullName} for {mod.Name}." ); Penumbra.Log.Debug($"Deleted directory {mod.ModPath.FullName} for {mod.Name}.");
} }
catch( Exception e ) catch (Exception e)
{ {
Penumbra.Log.Error( $"Could not delete the mod {mod.ModPath.Name}:\n{e}" ); Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}");
} }
}
ModPathChanged.Invoke( ModPathChangeType.Deleted, mod, mod.ModPath, null ); ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null);
_mods.RemoveAt( idx ); _mods.RemoveAt(idx);
foreach( var remainingMod in _mods.Skip( idx ) ) foreach (var remainingMod in _mods.Skip(idx))
{
--remainingMod.Index; --remainingMod.Index;
}
Penumbra.Log.Debug( $"Deleted mod {mod.Name}." ); Penumbra.Log.Debug($"Deleted mod {mod.Name}.");
} }
// Load a new mod and add it to the manager if successful. /// <summary> Load a new mod and add it to the manager if successful. </summary>
public void AddMod( DirectoryInfo modFolder ) public void AddMod(DirectoryInfo modFolder)
{ {
if( _mods.Any( m => m.ModPath.Name == modFolder.Name ) ) if (_mods.Any(m => m.ModPath.Name == modFolder.Name))
{
return; return;
}
Creator.SplitMultiGroups( modFolder ); Creator.SplitMultiGroups(modFolder);
var mod = LoadMod( modFolder, true ); var mod = LoadMod(this, modFolder, true);
if( mod == null ) if (mod == null)
{
return; return;
}
mod.Index = _mods.Count; mod.Index = _mods.Count;
_mods.Add( mod ); _mods.Add(mod);
ModPathChanged.Invoke( ModPathChangeType.Added, mod, null, mod.ModPath ); ModPathChanged.Invoke(ModPathChangeType.Added, mod, null, mod.ModPath);
Penumbra.Log.Debug( $"Added new mod {mod.Name} from {modFolder.FullName}." ); Penumbra.Log.Debug($"Added new mod {mod.Name} from {modFolder.FullName}.");
} }
public enum NewDirectoryState public enum NewDirectoryState
@ -162,66 +154,52 @@ public partial class Mod
Empty, Empty,
} }
// Return the state of the new potential name of a directory. /// <summary> Return the state of the new potential name of a directory. </summary>
public NewDirectoryState NewDirectoryValid( string oldName, string newName, out DirectoryInfo? directory ) public NewDirectoryState NewDirectoryValid(string oldName, string newName, out DirectoryInfo? directory)
{ {
directory = null; directory = null;
if( newName.Length == 0 ) if (newName.Length == 0)
{
return NewDirectoryState.Empty; return NewDirectoryState.Empty;
}
if( oldName == newName ) if (oldName == newName)
{
return NewDirectoryState.Identical; return NewDirectoryState.Identical;
}
var fixedNewName = Creator.ReplaceBadXivSymbols( newName ); var fixedNewName = Creator.ReplaceBadXivSymbols(newName);
if( fixedNewName != newName ) if (fixedNewName != newName)
{
return NewDirectoryState.ContainsInvalidSymbols; return NewDirectoryState.ContainsInvalidSymbols;
}
directory = new DirectoryInfo( Path.Combine( BasePath.FullName, fixedNewName ) ); directory = new DirectoryInfo(Path.Combine(BasePath.FullName, fixedNewName));
if( File.Exists( directory.FullName ) ) if (File.Exists(directory.FullName))
{
return NewDirectoryState.ExistsAsFile; return NewDirectoryState.ExistsAsFile;
}
if( !Directory.Exists( directory.FullName ) ) if (!Directory.Exists(directory.FullName))
{
return NewDirectoryState.NonExisting; return NewDirectoryState.NonExisting;
}
if( directory.EnumerateFileSystemInfos().Any() ) if (directory.EnumerateFileSystemInfos().Any())
{
return NewDirectoryState.ExistsNonEmpty; return NewDirectoryState.ExistsNonEmpty;
}
return NewDirectoryState.ExistsEmpty; return NewDirectoryState.ExistsEmpty;
} }
// Add new mods to NewMods and remove deleted mods from NewMods. /// <summary> Add new mods to NewMods and remove deleted mods from NewMods. </summary>
private void OnModPathChange( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
DirectoryInfo? newDirectory ) DirectoryInfo? newDirectory)
{ {
switch( type ) switch (type)
{ {
case ModPathChangeType.Added: case ModPathChangeType.Added:
NewMods.Add( mod ); NewMods.Add(mod);
break; break;
case ModPathChangeType.Deleted: case ModPathChangeType.Deleted:
NewMods.Remove( mod ); NewMods.Remove(mod);
break; break;
case ModPathChangeType.Moved: case ModPathChangeType.Moved:
if( oldDirectory != null && newDirectory != null ) if (oldDirectory != null && newDirectory != null)
{ DataEditor.MoveDataFile(oldDirectory, newDirectory);
MoveDataFile( oldDirectory, newDirectory );
}
break; break;
} }
} }
} }
} }

View file

@ -7,67 +7,6 @@ public sealed partial class Mod
{ {
public partial class Manager public partial class Manager
{ {
public void ChangeModFavorite( Index idx, bool state )
{
var mod = this[ idx ];
if( mod.Favorite != state )
{
mod.Favorite = state;
mod.SaveLocalData();
ModDataChanged?.Invoke( ModDataChangeType.Favorite, mod, null );
}
}
public void ChangeModNote( Index idx, string newNote )
{
var mod = this[ idx ];
if( mod.Note != newNote )
{
mod.Note = newNote;
mod.SaveLocalData();
ModDataChanged?.Invoke( ModDataChangeType.Favorite, mod, null );
}
}
private void ChangeTag( Index idx, int tagIdx, string newTag, bool local )
{
var mod = this[ idx ];
var which = local ? mod.LocalTags : mod.ModTags;
if( tagIdx < 0 || tagIdx > which.Count )
{
return;
}
ModDataChangeType flags = 0;
if( tagIdx == which.Count )
{
flags = mod.UpdateTags( local ? null : which.Append( newTag ), local ? which.Append( newTag ) : null );
}
else
{
var tmp = which.ToArray();
tmp[ tagIdx ] = newTag;
flags = mod.UpdateTags( local ? null : tmp, local ? tmp : null );
}
if( flags.HasFlag( ModDataChangeType.ModTags ) )
{
mod.SaveMeta();
}
if( flags.HasFlag( ModDataChangeType.LocalTags ) )
{
mod.SaveLocalData();
}
if( flags != 0 )
{
ModDataChanged?.Invoke( flags, mod, null );
}
}
public void ChangeLocalTag( Index idx, int tagIdx, string newTag )
=> ChangeTag( idx, tagIdx, newTag, true );
} }
} }

View file

@ -1,71 +0,0 @@
using System;
namespace Penumbra.Mods;
public sealed partial class Mod
{
public partial class Manager
{
public delegate void ModDataChangeDelegate( ModDataChangeType type, Mod mod, string? oldName );
public event ModDataChangeDelegate? ModDataChanged;
public void ChangeModName( Index idx, string newName )
{
var mod = this[ idx ];
if( mod.Name.Text != newName )
{
var oldName = mod.Name;
mod.Name = newName;
mod.SaveMeta();
ModDataChanged?.Invoke( ModDataChangeType.Name, mod, oldName.Text );
}
}
public void ChangeModAuthor( Index idx, string newAuthor )
{
var mod = this[ idx ];
if( mod.Author != newAuthor )
{
mod.Author = newAuthor;
mod.SaveMeta();
ModDataChanged?.Invoke( ModDataChangeType.Author, mod, null );
}
}
public void ChangeModDescription( Index idx, string newDescription )
{
var mod = this[ idx ];
if( mod.Description != newDescription )
{
mod.Description = newDescription;
mod.SaveMeta();
ModDataChanged?.Invoke( ModDataChangeType.Description, mod, null );
}
}
public void ChangeModVersion( Index idx, string newVersion )
{
var mod = this[ idx ];
if( mod.Version != newVersion )
{
mod.Version = newVersion;
mod.SaveMeta();
ModDataChanged?.Invoke( ModDataChangeType.Version, mod, null );
}
}
public void ChangeModWebsite( Index idx, string newWebsite )
{
var mod = this[ idx ];
if( mod.Website != newWebsite )
{
mod.Website = newWebsite;
mod.SaveMeta();
ModDataChanged?.Invoke( ModDataChangeType.Website, mod, null );
}
}
public void ChangeModTag( Index idx, int tagIdx, string newTag )
=> ChangeTag( idx, tagIdx, newTag, false );
}
}

View file

@ -305,18 +305,18 @@ public sealed partial class Mod
public bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message) public bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message)
{ {
var path = newName.RemoveInvalidPathSymbols(); var path = newName.RemoveInvalidPathSymbols();
if (path.Length == 0 if (path.Length != 0
|| mod.Groups.Any(o => !ReferenceEquals(o, group) && !mod.Groups.Any(o => !ReferenceEquals(o, group)
&& string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase))) && string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase)))
{ return true;
if (message)
_chat.NotificationMessage($"Could not name option {newName} because option with same filename {path} already exists.",
"Warning", NotificationType.Warning);
return false; if (message)
} Penumbra.ChatService.NotificationMessage(
$"Could not name option {newName} because option with same filename {path} already exists.",
"Warning", NotificationType.Warning);
return false;
return true;
} }
private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx)

View file

@ -97,7 +97,7 @@ public sealed partial class Mod
var queue = new ConcurrentQueue< Mod >(); var queue = new ConcurrentQueue< Mod >();
Parallel.ForEach( BasePath.EnumerateDirectories(), options, dir => Parallel.ForEach( BasePath.EnumerateDirectories(), options, dir =>
{ {
var mod = LoadMod( dir, false ); var mod = LoadMod( this, dir, false );
if( mod != null ) if( mod != null )
{ {
queue.Enqueue( mod ); queue.Enqueue( mod );

View file

@ -2,6 +2,7 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Penumbra.Services;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.Mods; namespace Penumbra.Mods;
@ -36,14 +37,16 @@ public sealed partial class Mod
IEnumerator IEnumerable.GetEnumerator() IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator(); => GetEnumerator();
private readonly Configuration _config; private readonly Configuration _config;
private readonly ChatService _chat; private readonly CommunicatorService _communicator;
public readonly ModDataEditor DataEditor;
public Manager(StartTracker time, Configuration config, ChatService chat) public Manager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor)
{ {
using var timer = time.Measure(StartTimeType.Mods); using var timer = time.Measure(StartTimeType.Mods);
_config = config; _config = config;
_chat = chat; _communicator = communicator;
DataEditor = dataEditor;
ModDirectoryChanged += OnModDirectoryChange; ModDirectoryChanged += OnModDirectoryChange;
SetBaseDirectory(config.ModDirectory, true); SetBaseDirectory(config.ModDirectory, true);
UpdateExportDirectory(_config.ExportDirectory, false); UpdateExportDirectory(_config.ExportDirectory, false);

View file

@ -0,0 +1,21 @@
using System;
namespace Penumbra.Mods;
[Flags]
public enum ModDataChangeType : ushort
{
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,
}

View file

@ -0,0 +1,362 @@
using System;
using System.IO;
using System.Linq;
using Dalamud.Utility;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.Services;
using Penumbra.Util;
namespace Penumbra.Mods;
public class ModDataEditor
{
private readonly FilenameService _filenameService;
private readonly SaveService _saveService;
private readonly CommunicatorService _communicatorService;
public ModDataEditor(FilenameService filenameService, SaveService saveService, CommunicatorService communicatorService)
{
_filenameService = filenameService;
_saveService = saveService;
_communicatorService = communicatorService;
}
public string MetaFile(Mod mod)
=> _filenameService.ModMetaPath(mod);
public string DataFile(Mod mod)
=> _filenameService.LocalDataFile(mod);
/// <summary> Create the file containing the meta information about a mod from scratch. </summary>
public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version,
string? website)
{
var mod = new Mod(directory);
mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString(name!);
mod.Author = author != null ? new LowerString(author) : mod.Author;
mod.Description = description ?? mod.Description;
mod.Version = version ?? mod.Version;
mod.Website = website ?? mod.Website;
_saveService.ImmediateSave(new ModMeta(mod));
}
public ModDataChangeType LoadLocalData(Mod mod)
{
var dataFile = _filenameService.LocalDataFile(mod);
var importDate = 0L;
var localTags = Enumerable.Empty<string>();
var favorite = false;
var note = string.Empty;
var save = true;
if (File.Exists(dataFile))
{
save = false;
try
{
var text = File.ReadAllText(dataFile);
var json = JObject.Parse(text);
importDate = json[nameof(Mod.ImportDate)]?.Value<long>() ?? importDate;
favorite = json[nameof(Mod.Favorite)]?.Value<bool>() ?? favorite;
note = json[nameof(Mod.Note)]?.Value<string>() ?? note;
localTags = json[nameof(Mod.LocalTags)]?.Values<string>().OfType<string>() ?? localTags;
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not load local mod data:\n{e}");
}
}
if (importDate == 0)
importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
ModDataChangeType changes = 0;
if (mod.ImportDate != importDate)
{
mod.ImportDate = importDate;
changes |= ModDataChangeType.ImportDate;
}
changes |= mod.UpdateTags(null, localTags);
if (mod.Favorite != favorite)
{
mod.Favorite = favorite;
changes |= ModDataChangeType.Favorite;
}
if (mod.Note != note)
{
mod.Note = note;
changes |= ModDataChangeType.Note;
}
if (save)
_saveService.QueueSave(new ModData(mod));
return changes;
}
public ModDataChangeType LoadMeta(Mod mod)
{
var metaFile = _filenameService.ModMetaPath(mod);
if (!File.Exists(metaFile))
{
Penumbra.Log.Debug($"No mod meta found for {mod.ModPath.Name}.");
return ModDataChangeType.Deletion;
}
try
{
var text = File.ReadAllText(metaFile);
var json = JObject.Parse(text);
var newName = json[nameof(Mod.Name)]?.Value<string>() ?? string.Empty;
var newAuthor = json[nameof(Mod.Author)]?.Value<string>() ?? string.Empty;
var newDescription = json[nameof(Mod.Description)]?.Value<string>() ?? string.Empty;
var newVersion = json[nameof(Mod.Version)]?.Value<string>() ?? string.Empty;
var newWebsite = json[nameof(Mod.Website)]?.Value<string>() ?? string.Empty;
var newFileVersion = json[nameof(Mod.FileVersion)]?.Value<uint>() ?? 0;
var importDate = json[nameof(Mod.ImportDate)]?.Value<long>();
var modTags = json[nameof(Mod.ModTags)]?.Values<string>().OfType<string>();
ModDataChangeType changes = 0;
if (mod.Name != newName)
{
changes |= ModDataChangeType.Name;
mod.Name = newName;
}
if (mod.Author != newAuthor)
{
changes |= ModDataChangeType.Author;
mod.Author = newAuthor;
}
if (mod.Description != newDescription)
{
changes |= ModDataChangeType.Description;
mod.Description = newDescription;
}
if (mod.Version != newVersion)
{
changes |= ModDataChangeType.Version;
mod.Version = newVersion;
}
if (mod.Website != newWebsite)
{
changes |= ModDataChangeType.Website;
mod.Website = newWebsite;
}
if (mod.FileVersion != newFileVersion)
{
mod.FileVersion = newFileVersion;
if (Mod.Migration.Migrate(mod, json))
{
changes |= ModDataChangeType.Migration;
_saveService.ImmediateSave(new ModMeta(mod));
}
}
if (importDate != null && mod.ImportDate != importDate.Value)
{
mod.ImportDate = importDate.Value;
changes |= ModDataChangeType.ImportDate;
}
changes |= mod.UpdateTags(modTags, null);
return changes;
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not load mod meta:\n{e}");
return ModDataChangeType.Deletion;
}
}
public void ChangeModName(Mod mod, string newName)
{
if (mod.Name.Text == newName)
return;
var oldName = mod.Name;
mod.Name = newName;
_saveService.QueueSave(new ModMeta(mod));
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Name, mod, oldName.Text);
}
public void ChangeModAuthor(Mod mod, string newAuthor)
{
if (mod.Author == newAuthor)
return;
mod.Author = newAuthor;
_saveService.QueueSave(new ModMeta(mod));
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Author, mod, null);
}
public void ChangeModDescription(Mod mod, string newDescription)
{
if (mod.Description == newDescription)
return;
mod.Description = newDescription;
_saveService.QueueSave(new ModMeta(mod));
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Description, mod, null);
}
public void ChangeModVersion(Mod mod, string newVersion)
{
if (mod.Version == newVersion)
return;
mod.Version = newVersion;
_saveService.QueueSave(new ModMeta(mod));
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Version, mod, null);
}
public void ChangeModWebsite(Mod mod, string newWebsite)
{
if (mod.Website == newWebsite)
return;
mod.Website = newWebsite;
_saveService.QueueSave(new ModMeta(mod));
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Website, mod, null);
}
public void ChangeModTag(Mod mod, int tagIdx, string newTag)
=> ChangeTag(mod, tagIdx, newTag, false);
public void ChangeLocalTag(Mod mod, int tagIdx, string newTag)
=> ChangeTag(mod, tagIdx, newTag, true);
public void ChangeModFavorite(Mod mod, bool state)
{
if (mod.Favorite == state)
return;
mod.Favorite = state;
_saveService.QueueSave(new ModData(mod));
;
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null);
}
public void ChangeModNote(Mod mod, string newNote)
{
if (mod.Note == newNote)
return;
mod.Note = newNote;
_saveService.QueueSave(new ModData(mod));
;
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null);
}
private void ChangeTag(Mod mod, int tagIdx, string newTag, bool local)
{
var which = local ? mod.LocalTags : mod.ModTags;
if (tagIdx < 0 || tagIdx > which.Count)
return;
ModDataChangeType flags = 0;
if (tagIdx == which.Count)
{
flags = mod.UpdateTags(local ? null : which.Append(newTag), local ? which.Append(newTag) : null);
}
else
{
var tmp = which.ToArray();
tmp[tagIdx] = newTag;
flags = mod.UpdateTags(local ? null : tmp, local ? tmp : null);
}
if (flags.HasFlag(ModDataChangeType.ModTags))
_saveService.QueueSave(new ModMeta(mod));
if (flags.HasFlag(ModDataChangeType.LocalTags))
_saveService.QueueSave(new ModData(mod));
if (flags != 0)
_communicatorService.ModDataChanged.Invoke(flags, mod, null);
}
public void MoveDataFile(DirectoryInfo oldMod, DirectoryInfo newMod)
{
var oldFile = _filenameService.LocalDataFile(oldMod.Name);
var newFile = _filenameService.LocalDataFile(newMod.Name);
if (!File.Exists(oldFile))
return;
try
{
File.Move(oldFile, newFile, true);
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not move local data file {oldFile} to {newFile}:\n{e}");
}
}
private readonly struct ModMeta : ISavable
{
private readonly Mod _mod;
public ModMeta(Mod mod)
=> _mod = mod;
public string ToFilename(FilenameService fileNames)
=> fileNames.ModMetaPath(_mod);
public void Save(StreamWriter writer)
{
var jObject = new JObject
{
{ nameof(Mod.FileVersion), JToken.FromObject(_mod.FileVersion) },
{ nameof(Mod.Name), JToken.FromObject(_mod.Name) },
{ nameof(Mod.Author), JToken.FromObject(_mod.Author) },
{ nameof(Mod.Description), JToken.FromObject(_mod.Description) },
{ nameof(Mod.Version), JToken.FromObject(_mod.Version) },
{ nameof(Mod.Website), JToken.FromObject(_mod.Website) },
{ nameof(Mod.ModTags), JToken.FromObject(_mod.ModTags) },
};
using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
jObject.WriteTo(jWriter);
}
}
private readonly struct ModData : ISavable
{
private readonly Mod _mod;
public ModData(Mod mod)
=> _mod = mod;
public string ToFilename(FilenameService fileNames)
=> fileNames.LocalDataFile(_mod);
public void Save(StreamWriter writer)
{
var jObject = new JObject
{
{ nameof(Mod.FileVersion), JToken.FromObject(_mod.FileVersion) },
{ nameof(Mod.ImportDate), JToken.FromObject(_mod.ImportDate) },
{ nameof(Mod.LocalTags), JToken.FromObject(_mod.LocalTags) },
{ nameof(Mod.Note), JToken.FromObject(_mod.Note) },
{ nameof(Mod.Favorite), JToken.FromObject(_mod.Favorite) },
};
using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
jObject.WriteTo(jWriter);
}
}
}

View file

@ -16,6 +16,8 @@ public enum ModPathChangeType
public partial class Mod public partial class Mod
{ {
public DirectoryInfo ModPath { get; private set; } public DirectoryInfo ModPath { get; private set; }
public string Identifier
=> Index >= 0 ? ModPath.Name : Name;
public int Index { get; private set; } = -1; public int Index { get; private set; } = -1;
public bool IsTemporary public bool IsTemporary
@ -31,7 +33,7 @@ public partial class Mod
_default = new SubMod( this ); _default = new SubMod( this );
} }
private static Mod? LoadMod( DirectoryInfo modPath, bool incorporateMetaChanges ) private static Mod? LoadMod( Manager modManager, DirectoryInfo modPath, bool incorporateMetaChanges )
{ {
modPath.Refresh(); modPath.Refresh();
if( !modPath.Exists ) if( !modPath.Exists )
@ -40,18 +42,17 @@ public partial class Mod
return null; return null;
} }
var mod = new Mod( modPath ); var mod = new Mod(modPath);
if( !mod.Reload( incorporateMetaChanges, out _ ) ) if (mod.Reload(modManager, incorporateMetaChanges, out _))
{ return mod;
// Can not be base path not existing because that is checked before.
Penumbra.Log.Warning( $"Mod at {modPath} without name is not supported." ); // Can not be base path not existing because that is checked before.
return null; Penumbra.Log.Warning( $"Mod at {modPath} without name is not supported." );
} return null;
return mod;
} }
internal bool Reload( bool incorporateMetaChanges, out ModDataChangeType modDataChange ) internal bool Reload(Manager modManager, bool incorporateMetaChanges, out ModDataChangeType modDataChange )
{ {
modDataChange = ModDataChangeType.Deletion; modDataChange = ModDataChangeType.Deletion;
ModPath.Refresh(); ModPath.Refresh();
@ -60,19 +61,19 @@ public partial class Mod
return false; return false;
} }
modDataChange = LoadMeta(); modDataChange = modManager.DataEditor.LoadMeta(this);
if( modDataChange.HasFlag( ModDataChangeType.Deletion ) || Name.Length == 0 ) if( modDataChange.HasFlag( ModDataChangeType.Deletion ) || Name.Length == 0 )
{ {
return false; return false;
} }
LoadLocalData(); modManager.DataEditor.LoadLocalData(this);
LoadDefaultOption(); LoadDefaultOption();
LoadAllGroups(); LoadAllGroups();
if( incorporateMetaChanges ) if( incorporateMetaChanges )
{ {
IncorporateAllMetaChanges( true ); IncorporateAllMetaChanges(true);
} }
ComputeChangedItems(); ComputeChangedItems();

View file

@ -64,19 +64,6 @@ public partial class Mod
return newModFolder.Length == 0 ? null : new DirectoryInfo( newModFolder ); return newModFolder.Length == 0 ? null : new DirectoryInfo( newModFolder );
} }
/// <summary> Create the file containing the meta information about a mod from scratch. </summary>
public static void CreateMeta( DirectoryInfo directory, string? name, string? author, string? description, string? version,
string? website )
{
var mod = new Mod( directory );
mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString( name! );
mod.Author = author != null ? new LowerString( author ) : mod.Author;
mod.Description = description ?? mod.Description;
mod.Version = version ?? mod.Version;
mod.Website = website ?? mod.Website;
mod.SaveMetaFile(); // Not delayed.
}
/// <summary> Create a file for an option group from given data. </summary> /// <summary> Create a file for an option group from given data. </summary>
public static void CreateOptionGroup( DirectoryInfo baseFolder, GroupType type, string name, public static void CreateOptionGroup( DirectoryInfo baseFolder, GroupType type, string name,
int priority, int index, uint defaultSettings, string desc, IEnumerable< ISubMod > subMods ) int priority, int index, uint defaultSettings, string desc, IEnumerable< ISubMod > subMods )
@ -147,13 +134,11 @@ public partial class Mod
internal static void CreateDefaultFiles( DirectoryInfo directory ) internal static void CreateDefaultFiles( DirectoryInfo directory )
{ {
var mod = new Mod( directory ); var mod = new Mod( directory );
mod.Reload( false, out _ ); mod.Reload( Penumbra.ModManager, false, out _ );
foreach( var file in mod.FindUnusedFiles() ) foreach( var file in mod.FindUnusedFiles() )
{ {
if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) ) if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) )
{
mod._default.FileData.TryAdd( gamePath, file ); mod._default.FileData.TryAdd( gamePath, file );
}
} }
mod._default.IncorporateMetaChanges( directory, true ); mod._default.IncorporateMetaChanges( directory, true );

View file

@ -3,145 +3,30 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Dalamud.Plugin; using Penumbra.Services;
using Newtonsoft.Json;
using Penumbra.Services;
namespace Penumbra.Mods; namespace Penumbra.Mods;
public sealed partial class Mod public sealed partial class Mod
{ {
public static DirectoryInfo LocalDataDirectory(DalamudPluginInterface pi) public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds();
=> new(Path.Combine( pi.ConfigDirectory.FullName, "mod_data" ));
public long ImportDate { get; private set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); public IReadOnlyList<string> LocalTags { get; private set; } = Array.Empty<string>();
public IReadOnlyList< string > LocalTags { get; private set; } = Array.Empty< string >(); public string AllTagsLower { get; private set; } = string.Empty;
public string Note { get; internal set; } = string.Empty;
public bool Favorite { get; internal set; } = false;
public string AllTagsLower { get; private set; } = string.Empty; internal ModDataChangeType UpdateTags(IEnumerable<string>? newModTags, IEnumerable<string>? newLocalTags)
public string Note { get; private set; } = string.Empty;
public bool Favorite { get; private set; } = false;
private FileInfo LocalDataFile
=> new(Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{ModPath.Name}.json" ));
private ModDataChangeType LoadLocalData()
{ {
var dataFile = LocalDataFile; if (newModTags == null && newLocalTags == null)
var importDate = 0L;
var localTags = Enumerable.Empty< string >();
var favorite = false;
var note = string.Empty;
var save = true;
if( File.Exists( dataFile.FullName ) )
{
save = false;
try
{
var text = File.ReadAllText( dataFile.FullName );
var json = JObject.Parse( text );
importDate = json[ nameof( ImportDate ) ]?.Value< long >() ?? importDate;
favorite = json[ nameof( Favorite ) ]?.Value< bool >() ?? favorite;
note = json[ nameof( Note ) ]?.Value< string >() ?? note;
localTags = json[ nameof( LocalTags ) ]?.Values< string >().OfType< string >() ?? localTags;
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not load local mod data:\n{e}" );
}
}
if( importDate == 0 )
{
importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
}
ModDataChangeType changes = 0;
if( ImportDate != importDate )
{
ImportDate = importDate;
changes |= ModDataChangeType.ImportDate;
}
changes |= UpdateTags( null, localTags );
if( Favorite != favorite )
{
Favorite = favorite;
changes |= ModDataChangeType.Favorite;
}
if( Note != note )
{
Note = note;
changes |= ModDataChangeType.Note;
}
if( save )
{
SaveLocalDataFile();
}
return changes;
}
private void SaveLocalData()
=> Penumbra.Framework.RegisterDelayed( nameof( SaveLocalData ) + ModPath.Name, SaveLocalDataFile );
private void SaveLocalDataFile()
{
var dataFile = LocalDataFile;
try
{
var jObject = new JObject
{
{ nameof( FileVersion ), JToken.FromObject( FileVersion ) },
{ nameof( ImportDate ), JToken.FromObject( ImportDate ) },
{ nameof( LocalTags ), JToken.FromObject( LocalTags ) },
{ nameof( Note ), JToken.FromObject( Note ) },
{ nameof( Favorite ), JToken.FromObject( Favorite ) },
};
dataFile.Directory!.Create();
File.WriteAllText( dataFile.FullName, jObject.ToString( Formatting.Indented ) );
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not write local data file for mod {Name} to {dataFile.FullName}:\n{e}" );
}
}
private static void MoveDataFile( DirectoryInfo oldMod, DirectoryInfo newMod )
{
var oldFile = Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{oldMod.Name}.json" );
var newFile = Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{newMod.Name}.json" );
if( File.Exists( oldFile ) )
{
try
{
File.Move( oldFile, newFile, true );
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not move local data file {oldFile} to {newFile}:\n{e}" );
}
}
}
private ModDataChangeType UpdateTags( IEnumerable< string >? newModTags, IEnumerable< string >? newLocalTags )
{
if( newModTags == null && newLocalTags == null )
{
return 0; return 0;
}
ModDataChangeType type = 0; ModDataChangeType type = 0;
if( newModTags != null ) if (newModTags != null)
{ {
var modTags = newModTags.Where( t => t.Length > 0 ).Distinct().ToArray(); var modTags = newModTags.Where(t => t.Length > 0).Distinct().ToArray();
if( !modTags.SequenceEqual( ModTags ) ) if (!modTags.SequenceEqual(ModTags))
{ {
newLocalTags ??= LocalTags; newLocalTags ??= LocalTags;
ModTags = modTags; ModTags = modTags;
@ -149,21 +34,19 @@ public sealed partial class Mod
} }
} }
if( newLocalTags != null ) if (newLocalTags != null)
{ {
var localTags = newLocalTags!.Where( t => t.Length > 0 && !ModTags.Contains( t ) ).Distinct().ToArray(); var localTags = newLocalTags!.Where(t => t.Length > 0 && !ModTags.Contains(t)).Distinct().ToArray();
if( !localTags.SequenceEqual( LocalTags ) ) if (!localTags.SequenceEqual(LocalTags))
{ {
LocalTags = localTags; LocalTags = localTags;
type |= ModDataChangeType.LocalTags; type |= ModDataChangeType.LocalTags;
} }
} }
if( type != 0 ) if (type != 0)
{ AllTagsLower = string.Join('\0', ModTags.Concat(LocalTags).Select(s => s.ToLowerInvariant()));
AllTagsLower = string.Join( '\0', ModTags.Concat( LocalTags ).Select( s => s.ToLowerInvariant() ) );
}
return type; return type;
} }
} }

View file

@ -13,146 +13,115 @@ namespace Penumbra.Mods;
public sealed partial class Mod public sealed partial class Mod
{ {
private static class Migration public static partial class Migration
{ {
public static bool Migrate( Mod mod, JObject json ) public static bool Migrate(Mod mod, JObject json)
{ => MigrateV0ToV1(mod, json) || MigrateV1ToV2(mod) || MigrateV2ToV3(mod);
var ret = MigrateV0ToV1( mod, json ) || MigrateV1ToV2( mod ) || MigrateV2ToV3( mod );
if( ret )
{
// Immediately save on migration.
mod.SaveMetaFile();
}
return ret; private static bool MigrateV2ToV3(Mod mod)
}
private static bool MigrateV2ToV3( Mod mod )
{ {
if( mod.FileVersion > 2 ) if (mod.FileVersion > 2)
{
return false; return false;
}
// Remove import time. // Remove import time.
mod.FileVersion = 3; mod.FileVersion = 3;
return true; return true;
} }
[GeneratedRegex(@"group_\d{3}_", RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture)]
private static partial Regex GroupRegex();
private static readonly Regex GroupRegex = new( @"group_\d{3}_", RegexOptions.Compiled ); private static bool MigrateV1ToV2(Mod mod)
private static bool MigrateV1ToV2( Mod mod )
{ {
if( mod.FileVersion > 1 ) if (mod.FileVersion > 1)
{
return false; return false;
}
if (!mod.GroupFiles.All( g => GroupRegex.IsMatch( g.Name ))) if (!mod.GroupFiles.All(g => GroupRegex().IsMatch(g.Name)))
{ foreach (var (group, index) in mod.GroupFiles.WithIndex().ToArray())
foreach( var (group, index) in mod.GroupFiles.WithIndex().ToArray() )
{ {
var newName = Regex.Replace( group.Name, "^group_", $"group_{index + 1:D3}_", RegexOptions.Compiled ); var newName = Regex.Replace(group.Name, "^group_", $"group_{index + 1:D3}_", RegexOptions.Compiled);
try try
{ {
if( newName != group.Name ) if (newName != group.Name)
{ group.MoveTo(Path.Combine(group.DirectoryName ?? string.Empty, newName), false);
group.MoveTo( Path.Combine( group.DirectoryName ?? string.Empty, newName ), false );
}
} }
catch( Exception e ) catch (Exception e)
{ {
Penumbra.Log.Error( $"Could not rename group file {group.Name} to {newName} during migration:\n{e}" ); Penumbra.Log.Error($"Could not rename group file {group.Name} to {newName} during migration:\n{e}");
} }
} }
}
mod.FileVersion = 2; mod.FileVersion = 2;
return true; return true;
} }
private static bool MigrateV0ToV1( Mod mod, JObject json ) private static bool MigrateV0ToV1(Mod mod, JObject json)
{ {
if( mod.FileVersion > 0 ) if (mod.FileVersion > 0)
{
return false; return false;
}
var swaps = json[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >() var swaps = json["FileSwaps"]?.ToObject<Dictionary<Utf8GamePath, FullPath>>()
?? new Dictionary< Utf8GamePath, FullPath >(); ?? new Dictionary<Utf8GamePath, FullPath>();
var groups = json[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >(); var groups = json["Groups"]?.ToObject<Dictionary<string, OptionGroupV0>>() ?? new Dictionary<string, OptionGroupV0>();
var priority = 1; var priority = 1;
var seenMetaFiles = new HashSet< FullPath >(); var seenMetaFiles = new HashSet<FullPath>();
foreach( var group in groups.Values ) foreach (var group in groups.Values)
{ ConvertGroup(mod, group, ref priority, seenMetaFiles);
ConvertGroup( mod, group, ref priority, seenMetaFiles );
}
foreach( var unusedFile in mod.FindUnusedFiles().Where( f => !seenMetaFiles.Contains( f ) ) ) foreach (var unusedFile in mod.FindUnusedFiles().Where(f => !seenMetaFiles.Contains(f)))
{ {
if( unusedFile.ToGamePath( mod.ModPath, out var gamePath ) if (unusedFile.ToGamePath(mod.ModPath, out var gamePath)
&& !mod._default.FileData.TryAdd( gamePath, unusedFile ) ) && !mod._default.FileData.TryAdd(gamePath, unusedFile))
{ Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod._default.FileData[gamePath]}.");
Penumbra.Log.Error( $"Could not add {gamePath} because it already points to {mod._default.FileData[ gamePath ]}." );
}
} }
mod._default.FileSwapData.Clear(); mod._default.FileSwapData.Clear();
mod._default.FileSwapData.EnsureCapacity( swaps.Count ); mod._default.FileSwapData.EnsureCapacity(swaps.Count);
foreach( var (gamePath, swapPath) in swaps ) foreach (var (gamePath, swapPath) in swaps)
{ mod._default.FileSwapData.Add(gamePath, swapPath);
mod._default.FileSwapData.Add( gamePath, swapPath );
}
mod._default.IncorporateMetaChanges( mod.ModPath, true ); mod._default.IncorporateMetaChanges(mod.ModPath, true);
foreach( var (group, index) in mod.Groups.WithIndex() ) foreach (var (group, index) in mod.Groups.WithIndex())
{ IModGroup.Save(group, mod.ModPath, index);
IModGroup.Save( group, mod.ModPath, index );
}
// Delete meta files. // Delete meta files.
foreach( var file in seenMetaFiles.Where( f => f.Exists ) ) foreach (var file in seenMetaFiles.Where(f => f.Exists))
{ {
try try
{ {
File.Delete( file.FullName ); File.Delete(file.FullName);
} }
catch( Exception e ) catch (Exception e)
{ {
Penumbra.Log.Warning( $"Could not delete meta file {file.FullName} during migration:\n{e}" ); Penumbra.Log.Warning($"Could not delete meta file {file.FullName} during migration:\n{e}");
} }
} }
// Delete old meta files. // Delete old meta files.
var oldMetaFile = Path.Combine( mod.ModPath.FullName, "metadata_manipulations.json" ); var oldMetaFile = Path.Combine(mod.ModPath.FullName, "metadata_manipulations.json");
if( File.Exists( oldMetaFile ) ) if (File.Exists(oldMetaFile))
{
try try
{ {
File.Delete( oldMetaFile ); File.Delete(oldMetaFile);
} }
catch( Exception e ) catch (Exception e)
{ {
Penumbra.Log.Warning( $"Could not delete old meta file {oldMetaFile} during migration:\n{e}" ); Penumbra.Log.Warning($"Could not delete old meta file {oldMetaFile} during migration:\n{e}");
} }
}
mod.FileVersion = 1; mod.FileVersion = 1;
mod.SaveDefaultMod(); mod.SaveDefaultMod();
mod.SaveMetaFile();
return true; return true;
} }
private static void ConvertGroup( Mod mod, OptionGroupV0 group, ref int priority, HashSet< FullPath > seenMetaFiles ) private static void ConvertGroup(Mod mod, OptionGroupV0 group, ref int priority, HashSet<FullPath> seenMetaFiles)
{ {
if( group.Options.Count == 0 ) if (group.Options.Count == 0)
{
return; return;
}
switch( group.SelectionType ) switch (group.SelectionType)
{ {
case GroupType.Multi: case GroupType.Multi:
@ -163,17 +132,15 @@ public sealed partial class Mod
Priority = priority++, Priority = priority++,
Description = string.Empty, Description = string.Empty,
}; };
mod._groups.Add( newMultiGroup ); mod._groups.Add(newMultiGroup);
foreach( var option in group.Options ) foreach (var option in group.Options)
{ newMultiGroup.PrioritizedOptions.Add((SubModFromOption(mod, option, seenMetaFiles), optionPriority++));
newMultiGroup.PrioritizedOptions.Add( ( SubModFromOption( mod, option, seenMetaFiles ), optionPriority++ ) );
}
break; break;
case GroupType.Single: case GroupType.Single:
if( group.Options.Count == 1 ) if (group.Options.Count == 1)
{ {
AddFilesToSubMod( mod._default, mod.ModPath, group.Options[ 0 ], seenMetaFiles ); AddFilesToSubMod(mod._default, mod.ModPath, group.Options[0], seenMetaFiles);
return; return;
} }
@ -183,38 +150,32 @@ public sealed partial class Mod
Priority = priority++, Priority = priority++,
Description = string.Empty, Description = string.Empty,
}; };
mod._groups.Add( newSingleGroup ); mod._groups.Add(newSingleGroup);
foreach( var option in group.Options ) foreach (var option in group.Options)
{ newSingleGroup.OptionData.Add(SubModFromOption(mod, option, seenMetaFiles));
newSingleGroup.OptionData.Add( SubModFromOption( mod, option, seenMetaFiles ) );
}
break; break;
} }
} }
private static void AddFilesToSubMod( SubMod mod, DirectoryInfo basePath, OptionV0 option, HashSet< FullPath > seenMetaFiles ) private static void AddFilesToSubMod(SubMod mod, DirectoryInfo basePath, OptionV0 option, HashSet<FullPath> seenMetaFiles)
{ {
foreach( var (relPath, gamePaths) in option.OptionFiles ) foreach (var (relPath, gamePaths) in option.OptionFiles)
{ {
var fullPath = new FullPath( basePath, relPath ); var fullPath = new FullPath(basePath, relPath);
foreach( var gamePath in gamePaths ) foreach (var gamePath in gamePaths)
{ mod.FileData.TryAdd(gamePath, fullPath);
mod.FileData.TryAdd( gamePath, fullPath );
}
if( fullPath.Extension is ".meta" or ".rgsp" ) if (fullPath.Extension is ".meta" or ".rgsp")
{ seenMetaFiles.Add(fullPath);
seenMetaFiles.Add( fullPath );
}
} }
} }
private static SubMod SubModFromOption( Mod mod, OptionV0 option, HashSet< FullPath > seenMetaFiles ) private static SubMod SubModFromOption(Mod mod, OptionV0 option, HashSet<FullPath> seenMetaFiles)
{ {
var subMod = new SubMod( mod ) { Name = option.OptionName }; var subMod = new SubMod(mod) { Name = option.OptionName };
AddFilesToSubMod( subMod, mod.ModPath, option, seenMetaFiles ); AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles);
subMod.IncorporateMetaChanges( mod.ModPath, false ); subMod.IncorporateMetaChanges(mod.ModPath, false);
return subMod; return subMod;
} }
@ -223,8 +184,8 @@ public sealed partial class Mod
public string OptionName = string.Empty; public string OptionName = string.Empty;
public string OptionDesc = string.Empty; public string OptionDesc = string.Empty;
[JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< Utf8GamePath > ) )] [JsonProperty(ItemConverterType = typeof(SingleOrArrayConverter<Utf8GamePath>))]
public Dictionary< Utf8RelPath, HashSet< Utf8GamePath > > OptionFiles = new(); public Dictionary<Utf8RelPath, HashSet<Utf8GamePath>> OptionFiles = new();
public OptionV0() public OptionV0()
{ } { }
@ -234,53 +195,49 @@ public sealed partial class Mod
{ {
public string GroupName = string.Empty; public string GroupName = string.Empty;
[JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )] [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public GroupType SelectionType = GroupType.Single; public GroupType SelectionType = GroupType.Single;
public List< OptionV0 > Options = new(); public List<OptionV0> Options = new();
public OptionGroupV0() public OptionGroupV0()
{ } { }
} }
// Not used anymore, but required for migration. // Not used anymore, but required for migration.
private class SingleOrArrayConverter< T > : JsonConverter private class SingleOrArrayConverter<T> : JsonConverter
{ {
public override bool CanConvert( Type objectType ) public override bool CanConvert(Type objectType)
=> objectType == typeof( HashSet< T > ); => objectType == typeof(HashSet<T>);
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{ {
var token = JToken.Load( reader ); var token = JToken.Load(reader);
if( token.Type == JTokenType.Array ) if (token.Type == JTokenType.Array)
{ return token.ToObject<HashSet<T>>() ?? new HashSet<T>();
return token.ToObject< HashSet< T > >() ?? new HashSet< T >();
}
var tmp = token.ToObject< T >(); var tmp = token.ToObject<T>();
return tmp != null return tmp != null
? new HashSet< T > { tmp } ? new HashSet<T> { tmp }
: new HashSet< T >(); : new HashSet<T>();
} }
public override bool CanWrite public override bool CanWrite
=> true; => true;
public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{ {
writer.WriteStartArray(); writer.WriteStartArray();
if( value != null ) if (value != null)
{ {
var v = ( HashSet< T > )value; var v = (HashSet<T>)value;
foreach( var val in v ) foreach (var val in v)
{ serializer.Serialize(writer, val?.ToString());
serializer.Serialize( writer, val?.ToString() );
}
} }
writer.WriteEndArray(); writer.WriteEndArray();
} }
} }
} }
} }

View file

@ -1,31 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Classes; using OtterGui.Classes;
namespace Penumbra.Mods; namespace Penumbra.Mods;
[Flags]
public enum ModDataChangeType : ushort
{
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,
}
public sealed partial class Mod : IMod public sealed partial class Mod : IMod
{ {
public static readonly TemporaryMod ForcedFiles = new() public static readonly TemporaryMod ForcedFiles = new()
@ -36,122 +14,13 @@ public sealed partial class Mod : IMod
}; };
public const uint CurrentFileVersion = 3; public const uint CurrentFileVersion = 3;
public uint FileVersion { get; private set; } = CurrentFileVersion; public uint FileVersion { get; internal set; } = CurrentFileVersion;
public LowerString Name { get; private set; } = "New Mod"; public LowerString Name { get; internal set; } = "New Mod";
public LowerString Author { get; private set; } = LowerString.Empty; public LowerString Author { get; internal set; } = LowerString.Empty;
public string Description { get; private set; } = string.Empty; public string Description { get; internal set; } = string.Empty;
public string Version { get; private set; } = string.Empty; public string Version { get; internal set; } = string.Empty;
public string Website { get; private set; } = string.Empty; public string Website { get; internal set; } = string.Empty;
public IReadOnlyList< string > ModTags { get; private set; } = Array.Empty< string >(); public IReadOnlyList< string > ModTags { get; internal set; } = Array.Empty< string >();
internal FileInfo MetaFile
=> new(Path.Combine( ModPath.FullName, "meta.json" ));
private ModDataChangeType LoadMeta()
{
var metaFile = MetaFile;
if( !File.Exists( metaFile.FullName ) )
{
Penumbra.Log.Debug( $"No mod meta found for {ModPath.Name}." );
return ModDataChangeType.Deletion;
}
try
{
var text = File.ReadAllText( metaFile.FullName );
var json = JObject.Parse( text );
var newName = json[ nameof( Name ) ]?.Value< string >() ?? string.Empty;
var newAuthor = json[ nameof( Author ) ]?.Value< string >() ?? string.Empty;
var newDescription = json[ nameof( Description ) ]?.Value< string >() ?? string.Empty;
var newVersion = json[ nameof( Version ) ]?.Value< string >() ?? string.Empty;
var newWebsite = json[ nameof( Website ) ]?.Value< string >() ?? string.Empty;
var newFileVersion = json[ nameof( FileVersion ) ]?.Value< uint >() ?? 0;
var importDate = json[ nameof( ImportDate ) ]?.Value< long >();
var modTags = json[ nameof( ModTags ) ]?.Values< string >().OfType< string >();
ModDataChangeType changes = 0;
if( Name != newName )
{
changes |= ModDataChangeType.Name;
Name = newName;
}
if( Author != newAuthor )
{
changes |= ModDataChangeType.Author;
Author = newAuthor;
}
if( Description != newDescription )
{
changes |= ModDataChangeType.Description;
Description = newDescription;
}
if( Version != newVersion )
{
changes |= ModDataChangeType.Version;
Version = newVersion;
}
if( Website != newWebsite )
{
changes |= ModDataChangeType.Website;
Website = newWebsite;
}
if( FileVersion != newFileVersion )
{
FileVersion = newFileVersion;
if( Migration.Migrate( this, json ) )
{
changes |= ModDataChangeType.Migration;
}
}
if( importDate != null && ImportDate != importDate.Value )
{
ImportDate = importDate.Value;
changes |= ModDataChangeType.ImportDate;
}
changes |= UpdateTags( modTags, null );
return changes;
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not load mod meta:\n{e}" );
return ModDataChangeType.Deletion;
}
}
private void SaveMeta()
=> Penumbra.Framework.RegisterDelayed( nameof( SaveMetaFile ) + ModPath.Name, SaveMetaFile );
private void SaveMetaFile()
{
var metaFile = MetaFile;
try
{
var jObject = new JObject
{
{ nameof( FileVersion ), JToken.FromObject( FileVersion ) },
{ nameof( Name ), JToken.FromObject( Name ) },
{ nameof( Author ), JToken.FromObject( Author ) },
{ nameof( Description ), JToken.FromObject( Description ) },
{ nameof( Version ), JToken.FromObject( Version ) },
{ nameof( Website ), JToken.FromObject( Website ) },
{ nameof( ModTags ), JToken.FromObject( ModTags ) },
};
File.WriteAllText( metaFile.FullName, jObject.ToString( Formatting.Indented ) );
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not write meta file for mod {Name} to {metaFile.FullName}:\n{e}" );
}
}
public override string ToString() public override string ToString()
=> Name.Text; => Name.Text;

View file

@ -46,14 +46,14 @@ public sealed partial class Mod
_default.ManipulationData = manips; _default.ManipulationData = manips;
} }
public static void SaveTempCollection( ModCollection collection, string? character = null ) public static void SaveTempCollection( Mod.Manager modManager, ModCollection collection, string? character = null )
{ {
DirectoryInfo? dir = null; DirectoryInfo? dir = null;
try try
{ {
dir = Creator.CreateModFolder( Penumbra.ModManager.BasePath, collection.Name ); dir = Creator.CreateModFolder( Penumbra.ModManager.BasePath, collection.Name );
var fileDir = Directory.CreateDirectory( Path.Combine( dir.FullName, "files" ) ); var fileDir = Directory.CreateDirectory( Path.Combine( dir.FullName, "files" ) );
Creator.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor, modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor,
$"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null ); $"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null );
var mod = new Mod( dir ); var mod = new Mod( dir );
var defaultMod = mod._default; var defaultMod = mod._default;
@ -88,7 +88,7 @@ public sealed partial class Mod
} }
mod.SaveDefaultMod(); mod.SaveDefaultMod();
Penumbra.ModManager.AddMod( dir ); modManager.AddMod( dir );
Penumbra.Log.Information( $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Name}." ); Penumbra.Log.Information( $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Name}." );
} }
catch( Exception e ) catch( Exception e )

View file

@ -10,28 +10,30 @@ using Penumbra.Util;
namespace Penumbra.Mods; namespace Penumbra.Mods;
public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISaveable public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable
{ {
private readonly Mod.Manager _modManager; private readonly Mod.Manager _modManager;
private readonly FilenameService _files; private readonly CommunicatorService _communicator;
private readonly FilenameService _files;
// Create a new ModFileSystem from the currently loaded mods and the current sort order file. // Create a new ModFileSystem from the currently loaded mods and the current sort order file.
public ModFileSystem(Mod.Manager modManager, FilenameService files) public ModFileSystem(Mod.Manager modManager, CommunicatorService communicator, FilenameService files)
{ {
_modManager = modManager; _modManager = modManager;
_files = files; _communicator = communicator;
_files = files;
Reload(); Reload();
Changed += OnChange; Changed += OnChange;
_modManager.ModDiscoveryFinished += Reload; _modManager.ModDiscoveryFinished += Reload;
_modManager.ModDataChanged += OnDataChange; _communicator.ModDataChanged.Event += OnDataChange;
_modManager.ModPathChanged += OnModPathChange; _modManager.ModPathChanged += OnModPathChange;
} }
public void Dispose() public void Dispose()
{ {
_modManager.ModPathChanged -= OnModPathChange; _modManager.ModPathChanged -= OnModPathChange;
_modManager.ModDiscoveryFinished -= Reload; _modManager.ModDiscoveryFinished -= Reload;
_modManager.ModDataChanged -= OnDataChange; _communicator.ModDataChanged.Event -= OnDataChange;
} }
public struct ImportDate : ISortMode<Mod> public struct ImportDate : ISortMode<Mod>

View file

@ -92,6 +92,7 @@ public class PenumbraNew
// Add Mod Services // Add Mod Services
services.AddSingleton<TempModManager>() services.AddSingleton<TempModManager>()
.AddSingleton<ModDataEditor>()
.AddSingleton<Mod.Manager>() .AddSingleton<Mod.Manager>()
.AddSingleton<ModFileSystem>(); .AddSingleton<ModFileSystem>();

View file

@ -45,6 +45,13 @@ public class CommunicatorService : IDisposable
/// </list> </summary> /// </list> </summary>
public readonly EventWrapper<nint, string, nint> CreatedCharacterBase = new(nameof(CreatedCharacterBase)); public readonly EventWrapper<nint, string, nint> CreatedCharacterBase = new(nameof(CreatedCharacterBase));
/// <summary><list type="number">
/// <item>Parameter is the type of data change for the mod, which can be multiple flags. </item>
/// <item>Parameter is the changed mod. </item>
/// <item>Parameter is the old name of the mod in case of a name change, and null otherwise. </item>
/// </list> </summary>
public readonly EventWrapper<ModDataChangeType, Mod, string?> ModDataChanged = new(nameof(ModDataChanged));
public void Dispose() public void Dispose()
{ {
CollectionChange.Dispose(); CollectionChange.Dispose();
@ -52,5 +59,6 @@ public class CommunicatorService : IDisposable
ModMetaChange.Dispose(); ModMetaChange.Dispose();
CreatingCharacterBase.Dispose(); CreatingCharacterBase.Dispose();
CreatedCharacterBase.Dispose(); CreatedCharacterBase.Dispose();
ModDataChanged.Dispose();
} }
} }

View file

@ -38,7 +38,7 @@ public class FilenameService
/// <summary> Obtain the path of the local data file given a mod directory. Returns an empty string if the mod is temporary. </summary> /// <summary> Obtain the path of the local data file given a mod directory. Returns an empty string if the mod is temporary. </summary>
public string LocalDataFile(Mod mod) public string LocalDataFile(Mod mod)
=> mod.IsTemporary ? string.Empty : LocalDataFile(mod.ModPath.FullName); => LocalDataFile(mod.ModPath.FullName);
/// <summary> Obtain the path of the local data file given a mod directory. </summary> /// <summary> Obtain the path of the local data file given a mod directory. </summary>
public string LocalDataFile(string modDirectory) public string LocalDataFile(string modDirectory)
@ -66,7 +66,7 @@ public class FilenameService
/// <summary> Obtain the path of the meta file for a given mod. Returns an empty string if the mod is temporary. </summary> /// <summary> Obtain the path of the meta file for a given mod. Returns an empty string if the mod is temporary. </summary>
public string ModMetaPath(Mod mod) public string ModMetaPath(Mod mod)
=> mod.IsTemporary ? string.Empty : ModMetaPath(mod.ModPath.FullName); => ModMetaPath(mod.ModPath.FullName);
/// <summary> Obtain the path of the meta file given a mod directory. </summary> /// <summary> Obtain the path of the meta file given a mod directory. </summary>
public string ModMetaPath(string modDirectory) public string ModMetaPath(string modDirectory)

View file

@ -267,7 +267,7 @@ public class ItemSwapTab : IDisposable, ITab
private void CreateMod() private void CreateMod()
{ {
var newDir = Mod.Creator.CreateModFolder(_modManager.BasePath, _newModName); var newDir = Mod.Creator.CreateModFolder(_modManager.BasePath, _newModName);
Mod.Creator.CreateMeta(newDir, _newModName, _config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty); _modManager.DataEditor.CreateMeta(newDir, _newModName, _config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty);
Mod.Creator.CreateDefaultFiles(newDir); Mod.Creator.CreateDefaultFiles(newDir);
_modManager.AddMod(newDir); _modManager.AddMod(newDir);
if (!_swapData.WriteMod(_modManager.Last(), if (!_swapData.WriteMod(_modManager.Last(),

View file

@ -150,7 +150,7 @@ public partial class ModEditWindow : Window, IDisposable
_itemSwapTab.DrawContent(); _itemSwapTab.DrawContent();
} }
// A row of three buttonSizes and a help marker that can be used for material suffix changing. /// <summary> A row of three buttonSizes and a help marker that can be used for material suffix changing. </summary>
private static class MaterialSuffix private static class MaterialSuffix
{ {
private static string _materialSuffixFrom = string.Empty; private static string _materialSuffixFrom = string.Empty;

View file

@ -15,7 +15,7 @@ using OtterGui.Raii;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Import; using Penumbra.Import;
using Penumbra.Import.Structs; using Penumbra.Import.Structs;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
@ -78,7 +78,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
_communicator.CollectionChange.Event += OnCollectionChange; _communicator.CollectionChange.Event += OnCollectionChange;
_collectionManager.Current.ModSettingChanged += OnSettingChange; _collectionManager.Current.ModSettingChanged += OnSettingChange;
_collectionManager.Current.InheritanceChanged += OnInheritanceChange; _collectionManager.Current.InheritanceChanged += OnInheritanceChange;
_modManager.ModDataChanged += OnModDataChange; _communicator.ModDataChanged.Event += OnModDataChange;
_modManager.ModDiscoveryStarted += StoreCurrentSelection; _modManager.ModDiscoveryStarted += StoreCurrentSelection;
_modManager.ModDiscoveryFinished += RestoreLastSelection; _modManager.ModDiscoveryFinished += RestoreLastSelection;
OnCollectionChange(CollectionType.Current, null, _collectionManager.Current, ""); OnCollectionChange(CollectionType.Current, null, _collectionManager.Current, "");
@ -89,7 +89,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
base.Dispose(); base.Dispose();
_modManager.ModDiscoveryStarted -= StoreCurrentSelection; _modManager.ModDiscoveryStarted -= StoreCurrentSelection;
_modManager.ModDiscoveryFinished -= RestoreLastSelection; _modManager.ModDiscoveryFinished -= RestoreLastSelection;
_modManager.ModDataChanged -= OnModDataChange; _communicator.ModDataChanged.Event -= OnModDataChange;
_collectionManager.Current.ModSettingChanged -= OnSettingChange; _collectionManager.Current.ModSettingChanged -= OnSettingChange;
_collectionManager.Current.InheritanceChanged -= OnInheritanceChange; _collectionManager.Current.InheritanceChanged -= OnInheritanceChange;
_communicator.CollectionChange.Event -= OnCollectionChange; _communicator.CollectionChange.Event -= OnCollectionChange;
@ -127,7 +127,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
try try
{ {
var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName); var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName);
Mod.Creator.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty); _modManager.DataEditor.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty);
Mod.Creator.CreateDefaultFiles(newDir); Mod.Creator.CreateDefaultFiles(newDir);
Penumbra.ModManager.AddMod(newDir); Penumbra.ModManager.AddMod(newDir);
_newModName = string.Empty; _newModName = string.Empty;
@ -187,29 +187,29 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
private void ToggleLeafFavorite(FileSystem<Mod>.Leaf mod) private void ToggleLeafFavorite(FileSystem<Mod>.Leaf mod)
{ {
if (ImGui.MenuItem(mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite")) if (ImGui.MenuItem(mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite"))
_modManager.ChangeModFavorite(mod.Value.Index, !mod.Value.Favorite); _modManager.DataEditor.ChangeModFavorite(mod.Value, !mod.Value.Favorite);
} }
private void SetDefaultImportFolder(ModFileSystem.Folder folder) private void SetDefaultImportFolder(ModFileSystem.Folder folder)
{ {
if (ImGui.MenuItem("Set As Default Import Folder")) if (!ImGui.MenuItem("Set As Default Import Folder"))
{ return;
var newName = folder.FullName();
if (newName != _config.DefaultImportFolder) var newName = folder.FullName();
{ if (newName == _config.DefaultImportFolder)
_config.DefaultImportFolder = newName; return;
_config.Save();
} _config.DefaultImportFolder = newName;
} _config.Save();
} }
private void ClearDefaultImportFolder() private void ClearDefaultImportFolder()
{ {
if (ImGui.MenuItem("Clear Default Import Folder") && _config.DefaultImportFolder.Length > 0) if (!ImGui.MenuItem("Clear Default Import Folder") || _config.DefaultImportFolder.Length <= 0)
{ return;
_config.DefaultImportFolder = string.Empty;
_config.Save(); _config.DefaultImportFolder = string.Empty;
} _config.Save();
} }
private string _newModName = string.Empty; private string _newModName = string.Empty;
@ -241,7 +241,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
return; return;
_import = new TexToolsImporter(_modManager.BasePath, f.Count, f.Select(file => new FileInfo(file)), _import = new TexToolsImporter(_modManager.BasePath, f.Count, f.Select(file => new FileInfo(file)),
AddNewMod, _config, _modEditor); AddNewMod, _config, _modEditor, _modManager);
ImGui.OpenPopup("Import Status"); ImGui.OpenPopup("Import Status");
}, 0, modPath, _config.AlwaysOpenDefaultImport); }, 0, modPath, _config.AlwaysOpenDefaultImport);
} }

View file

@ -42,7 +42,7 @@ public class ModPanelDescriptionTab : ITab
out var editedTag); out var editedTag);
_tutorial.OpenTutorial(BasicTutorialSteps.Tags); _tutorial.OpenTutorial(BasicTutorialSteps.Tags);
if (tagIdx >= 0) if (tagIdx >= 0)
_modManager.ChangeLocalTag(_selector.Selected!.Index, tagIdx, editedTag); _modManager.DataEditor.ChangeLocalTag(_selector.Selected!, tagIdx, editedTag);
if (_selector.Selected!.ModTags.Count > 0) if (_selector.Selected!.ModTags.Count > 0)
_modTags.Draw("Mod Tags: ", "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod.", _modTags.Draw("Mod Tags: ", "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod.",

View file

@ -77,7 +77,7 @@ public class ModPanelEditTab : ITab
var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags,
out var editedTag); out var editedTag);
if (tagIdx >= 0) if (tagIdx >= 0)
_modManager.ChangeModTag(_mod.Index, tagIdx, editedTag); _modManager.DataEditor.ChangeModTag(_mod, tagIdx, editedTag);
UiHelpers.DefaultLineSpace(); UiHelpers.DefaultLineSpace();
AddOptionGroup.Draw(_modManager, _mod); AddOptionGroup.Draw(_modManager, _mod);
@ -172,18 +172,18 @@ public class ModPanelEditTab : ITab
private void EditRegularMeta() private void EditRegularMeta()
{ {
if (Input.Text("Name", Input.Name, Input.None, _mod.Name, out var newName, 256, UiHelpers.InputTextWidth.X)) if (Input.Text("Name", Input.Name, Input.None, _mod.Name, out var newName, 256, UiHelpers.InputTextWidth.X))
_modManager.ChangeModName(_mod.Index, newName); _modManager.DataEditor.ChangeModName(_mod, newName);
if (Input.Text("Author", Input.Author, Input.None, _mod.Author, out var newAuthor, 256, UiHelpers.InputTextWidth.X)) if (Input.Text("Author", Input.Author, Input.None, _mod.Author, out var newAuthor, 256, UiHelpers.InputTextWidth.X))
Penumbra.ModManager.ChangeModAuthor(_mod.Index, newAuthor); _modManager.DataEditor.ChangeModAuthor(_mod, newAuthor);
if (Input.Text("Version", Input.Version, Input.None, _mod.Version, out var newVersion, 32, if (Input.Text("Version", Input.Version, Input.None, _mod.Version, out var newVersion, 32,
UiHelpers.InputTextWidth.X)) UiHelpers.InputTextWidth.X))
_modManager.ChangeModVersion(_mod.Index, newVersion); _modManager.DataEditor.ChangeModVersion(_mod, newVersion);
if (Input.Text("Website", Input.Website, Input.None, _mod.Website, out var newWebsite, 256, if (Input.Text("Website", Input.Website, Input.None, _mod.Website, out var newWebsite, 256,
UiHelpers.InputTextWidth.X)) UiHelpers.InputTextWidth.X))
_modManager.ChangeModWebsite(_mod.Index, newWebsite); _modManager.DataEditor.ChangeModWebsite(_mod, newWebsite);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3)); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3));
@ -192,13 +192,13 @@ public class ModPanelEditTab : ITab
_delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, Input.Description)); _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, Input.Description));
ImGui.SameLine(); ImGui.SameLine();
var fileExists = File.Exists(_mod.MetaFile.FullName); var fileExists = File.Exists(_modManager.DataEditor.MetaFile(_mod));
var tt = fileExists var tt = fileExists
? "Open the metadata json file in the text editor of your choice." ? "Open the metadata json file in the text editor of your choice."
: "The metadata json file does not exist."; : "The metadata json file does not exist.";
if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.FileExport.ToIconString()}##metaFile", UiHelpers.IconButtonSize, tt, if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.FileExport.ToIconString()}##metaFile", UiHelpers.IconButtonSize, tt,
!fileExists, true)) !fileExists, true))
Process.Start(new ProcessStartInfo(_mod.MetaFile.FullName) { UseShellExecute = true }); Process.Start(new ProcessStartInfo(_modManager.DataEditor.MetaFile(_mod)) { UseShellExecute = true });
} }
/// <summary> Do some edits outside of iterations. </summary> /// <summary> Do some edits outside of iterations. </summary>
@ -349,7 +349,7 @@ public class ModPanelEditTab : ITab
switch (_newDescriptionIdx) switch (_newDescriptionIdx)
{ {
case Input.Description: case Input.Description:
modManager.ChangeModDescription(_mod.Index, _newDescription); modManager.DataEditor.ChangeModDescription(_mod, _newDescription);
break; break;
case >= 0: case >= 0:
if (_newDescriptionOptionIdx < 0) if (_newDescriptionOptionIdx < 0)

View file

@ -140,7 +140,7 @@ public class ModPanelTabBar
ImGui.SetCursorPos(newPos); ImGui.SetCursorPos(newPos);
if (ImGui.Button(FontAwesomeIcon.Star.ToIconString())) if (ImGui.Button(FontAwesomeIcon.Star.ToIconString()))
_modManager.ChangeModFavorite(mod.Index, !mod.Favorite); _modManager.DataEditor.ChangeModFavorite(mod, !mod.Favorite);
} }
var hovered = ImGui.IsItemHovered(); var hovered = ImGui.IsItemHovered();

View file

@ -1,10 +1,8 @@
using System; using System;
using System.IO; using System.IO;
using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Log; using OtterGui.Log;
using Penumbra.Api;
using Penumbra.Services; using Penumbra.Services;
namespace Penumbra.Util; namespace Penumbra.Util;
@ -12,7 +10,7 @@ namespace Penumbra.Util;
/// <summary> /// <summary>
/// Any file type that we want to save via SaveService. /// Any file type that we want to save via SaveService.
/// </summary> /// </summary>
public interface ISaveable public interface ISavable
{ {
/// <summary> The full file name of a given object. </summary> /// <summary> The full file name of a given object. </summary>
public string ToFilename(FilenameService fileNames); public string ToFilename(FilenameService fileNames);
@ -42,7 +40,7 @@ public class SaveService
} }
/// <summary> Queue a save for the next framework tick. </summary> /// <summary> Queue a save for the next framework tick. </summary>
public void QueueSave(ISaveable value) public void QueueSave(ISavable value)
{ {
var file = value.ToFilename(_fileNames); var file = value.ToFilename(_fileNames);
_framework.RegisterDelayed(value.GetType().Name + file, () => _framework.RegisterDelayed(value.GetType().Name + file, () =>
@ -52,7 +50,7 @@ public class SaveService
} }
/// <summary> Immediately trigger a save. </summary> /// <summary> Immediately trigger a save. </summary>
public void ImmediateSave(ISaveable value) public void ImmediateSave(ISavable value)
{ {
var name = value.ToFilename(_fileNames); var name = value.ToFilename(_fileNames);
try try
@ -75,7 +73,7 @@ public class SaveService
} }
} }
public void ImmediateDelete(ISaveable value) public void ImmediateDelete(ISavable value)
{ {
var name = value.ToFilename(_fileNames); var name = value.ToFilename(_fileNames);
try try