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 Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Mods;
namespace Penumbra.Api;
@ -111,7 +112,7 @@ public class PenumbraIpcProviders : IDisposable
internal readonly FuncProvider< string, int, PenumbraApiEc > RemoveTemporaryModAll;
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;
@ -219,7 +220,7 @@ public class PenumbraIpcProviders : IDisposable
RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider( pi, Api.RemoveTemporaryModAll );
RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider( pi, Api.RemoveTemporaryMod );
Tester = new IpcTester( pi, this );
Tester = new IpcTester( pi, this, modManager );
Initialized.Invoke();
}

View file

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

View file

@ -11,7 +11,7 @@ using Penumbra.Util;
namespace Penumbra.Collections;
// 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,
// 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.Widgets;
using Penumbra.GameData.Enums;
using Penumbra.Import.Structs;
using Penumbra.Import.Structs;
using Penumbra.Mods;
using Penumbra.Services;
using Penumbra.UI;
using Penumbra.UI.Classes;
using Penumbra.Util;
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
namespace Penumbra;
[Serializable]
public class Configuration : IPluginConfiguration
public class Configuration : IPluginConfiguration, ISavable
{
[JsonIgnore]
private readonly string _fileName;
[JsonIgnore]
private readonly FrameworkManager _framework;
private readonly SaveService _saveService;
public int Version { get; set; } = Constants.CurrentVersion;
@ -101,14 +99,13 @@ public class Configuration : IPluginConfiguration
/// Load the current configuration.
/// Includes adding new colors and migrating from old versions.
/// </summary>
public Configuration(FilenameService fileNames, ConfigMigrationService migrator, FrameworkManager framework)
public Configuration(FilenameService fileNames, ConfigMigrationService migrator, SaveService saveService)
{
_fileName = fileNames.ConfigFile;
_framework = framework;
Load(migrator);
_saveService = saveService;
Load(fileNames, migrator);
}
public void Load(ConfigMigrationService migrator)
public void Load(FilenameService fileNames, ConfigMigrationService migrator)
{
static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs)
{
@ -117,9 +114,9 @@ public class Configuration : IPluginConfiguration
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
{
Error = HandleDeserializationError,
@ -130,21 +127,8 @@ public class Configuration : IPluginConfiguration
}
/// <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()
=> _framework.RegisterDelayed(nameof(SaveConfiguration), SaveConfiguration);
=> _saveService.QueueSave(this);
/// <summary> Contains some default values or boundaries for config values. </summary>
public static class Constants
@ -192,4 +176,14 @@ public class Configuration : IPluginConfiguration
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.Tasks;
using Newtonsoft.Json;
using Penumbra.Api;
using Penumbra.Import.Structs;
using Penumbra.Mods;
using FileMode = System.IO.FileMode;
@ -33,22 +34,19 @@ public partial class TexToolsImporter : IDisposable
public ImporterState State { get; private set; }
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 ModEditor _editor;
private readonly ModEditor _editor;
private readonly Mod.Manager _modManager;
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;
_tmpFile = Path.Combine( _baseDirectory.FullName, TempFileName );
_modPackFiles = modPackFiles;
_config = config;
_editor = editor;
_editor = editor;
_modManager = modManager;
_modPackCount = count;
ExtractedMods = new List< (FileInfo, DirectoryInfo?, Exception?) >( count );
_token = _cancellation.Token;

View file

@ -35,7 +35,7 @@ public partial class TexToolsImporter
_currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) );
// 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
_streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" );
@ -90,7 +90,7 @@ public partial class TexToolsImporter
Penumbra.Log.Information( " -> Importing Simple V2 ModPack" );
_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"
: modList.Description, modList.Version, modList.Url );
@ -135,7 +135,7 @@ public partial class TexToolsImporter
_currentModName = modList.Name;
_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 )
{

View file

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

View file

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

View file

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

View file

@ -7,67 +7,6 @@ public sealed partial class Mod
{
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)
{
var path = newName.RemoveInvalidPathSymbols();
if (path.Length == 0
|| mod.Groups.Any(o => !ReferenceEquals(o, group)
if (path.Length != 0
&& !mod.Groups.Any(o => !ReferenceEquals(o, group)
&& string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase)))
{
if (message)
_chat.NotificationMessage($"Could not name option {newName} because option with same filename {path} already exists.",
"Warning", NotificationType.Warning);
return true;
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)

View file

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

View file

@ -2,6 +2,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Penumbra.Services;
using Penumbra.Util;
namespace Penumbra.Mods;
@ -36,14 +37,16 @@ public sealed partial class Mod
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
private readonly Configuration _config;
private readonly ChatService _chat;
private readonly Configuration _config;
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);
_config = config;
_chat = chat;
_communicator = communicator;
DataEditor = dataEditor;
ModDirectoryChanged += OnModDirectoryChange;
SetBaseDirectory(config.ModDirectory, true);
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 DirectoryInfo ModPath { get; private set; }
public string Identifier
=> Index >= 0 ? ModPath.Name : Name;
public int Index { get; private set; } = -1;
public bool IsTemporary
@ -31,7 +33,7 @@ public partial class Mod
_default = new SubMod( this );
}
private static Mod? LoadMod( DirectoryInfo modPath, bool incorporateMetaChanges )
private static Mod? LoadMod( Manager modManager, DirectoryInfo modPath, bool incorporateMetaChanges )
{
modPath.Refresh();
if( !modPath.Exists )
@ -40,18 +42,17 @@ public partial class Mod
return null;
}
var mod = new Mod( modPath );
if( !mod.Reload( incorporateMetaChanges, out _ ) )
{
// Can not be base path not existing because that is checked before.
Penumbra.Log.Warning( $"Mod at {modPath} without name is not supported." );
return null;
}
var mod = new Mod(modPath);
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." );
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;
ModPath.Refresh();
@ -60,19 +61,19 @@ public partial class Mod
return false;
}
modDataChange = LoadMeta();
modDataChange = modManager.DataEditor.LoadMeta(this);
if( modDataChange.HasFlag( ModDataChangeType.Deletion ) || Name.Length == 0 )
{
return false;
}
LoadLocalData();
modManager.DataEditor.LoadLocalData(this);
LoadDefaultOption();
LoadAllGroups();
if( incorporateMetaChanges )
{
IncorporateAllMetaChanges( true );
IncorporateAllMetaChanges(true);
}
ComputeChangedItems();

View file

@ -64,19 +64,6 @@ public partial class Mod
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>
public static void CreateOptionGroup( DirectoryInfo baseFolder, GroupType type, string name,
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 )
{
var mod = new Mod( directory );
mod.Reload( false, out _ );
mod.Reload( Penumbra.ModManager, false, out _ );
foreach( var file in mod.FindUnusedFiles() )
{
if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) )
{
mod._default.FileData.TryAdd( gamePath, file );
}
}
mod._default.IncorporateMetaChanges( directory, true );

View file

@ -3,145 +3,30 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Plugin;
using Newtonsoft.Json;
using Penumbra.Services;
using Penumbra.Services;
namespace Penumbra.Mods;
public sealed partial class Mod
{
public static DirectoryInfo LocalDataDirectory(DalamudPluginInterface pi)
=> new(Path.Combine( pi.ConfigDirectory.FullName, "mod_data" ));
public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds();
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;
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()
internal ModDataChangeType UpdateTags(IEnumerable<string>? newModTags, IEnumerable<string>? newLocalTags)
{
var dataFile = LocalDataFile;
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 )
{
if (newModTags == null && newLocalTags == null)
return 0;
}
ModDataChangeType type = 0;
if( newModTags != null )
if (newModTags != null)
{
var modTags = newModTags.Where( t => t.Length > 0 ).Distinct().ToArray();
if( !modTags.SequenceEqual( ModTags ) )
var modTags = newModTags.Where(t => t.Length > 0).Distinct().ToArray();
if (!modTags.SequenceEqual(ModTags))
{
newLocalTags ??= LocalTags;
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();
if( !localTags.SequenceEqual( LocalTags ) )
var localTags = newLocalTags!.Where(t => t.Length > 0 && !ModTags.Contains(t)).Distinct().ToArray();
if (!localTags.SequenceEqual(LocalTags))
{
LocalTags = localTags;
type |= ModDataChangeType.LocalTags;
}
}
if( type != 0 )
{
AllTagsLower = string.Join( '\0', ModTags.Concat( LocalTags ).Select( s => s.ToLowerInvariant() ) );
}
if (type != 0)
AllTagsLower = string.Join('\0', ModTags.Concat(LocalTags).Select(s => s.ToLowerInvariant()));
return type;
}
}
}

View file

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

View file

@ -1,31 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
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 static readonly TemporaryMod ForcedFiles = new()
@ -36,122 +14,13 @@ public sealed partial class Mod : IMod
};
public const uint CurrentFileVersion = 3;
public uint FileVersion { get; private set; } = CurrentFileVersion;
public LowerString Name { get; private set; } = "New Mod";
public LowerString Author { get; private set; } = LowerString.Empty;
public string Description { get; private set; } = string.Empty;
public string Version { get; private set; } = string.Empty;
public string Website { get; private set; } = string.Empty;
public IReadOnlyList< string > ModTags { get; private 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 uint FileVersion { get; internal set; } = CurrentFileVersion;
public LowerString Name { get; internal set; } = "New Mod";
public LowerString Author { get; internal set; } = LowerString.Empty;
public string Description { get; internal set; } = string.Empty;
public string Version { get; internal set; } = string.Empty;
public string Website { get; internal set; } = string.Empty;
public IReadOnlyList< string > ModTags { get; internal set; } = Array.Empty< string >();
public override string ToString()
=> Name.Text;

View file

@ -46,14 +46,14 @@ public sealed partial class Mod
_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;
try
{
dir = Creator.CreateModFolder( Penumbra.ModManager.BasePath, collection.Name );
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 );
var mod = new Mod( dir );
var defaultMod = mod._default;
@ -88,7 +88,7 @@ public sealed partial class Mod
}
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}." );
}
catch( Exception e )

View file

@ -10,28 +10,30 @@ using Penumbra.Util;
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 FilenameService _files;
private readonly Mod.Manager _modManager;
private readonly CommunicatorService _communicator;
private readonly FilenameService _files;
// 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;
_files = files;
_modManager = modManager;
_communicator = communicator;
_files = files;
Reload();
Changed += OnChange;
_modManager.ModDiscoveryFinished += Reload;
_modManager.ModDataChanged += OnDataChange;
_modManager.ModPathChanged += OnModPathChange;
Changed += OnChange;
_modManager.ModDiscoveryFinished += Reload;
_communicator.ModDataChanged.Event += OnDataChange;
_modManager.ModPathChanged += OnModPathChange;
}
public void Dispose()
{
_modManager.ModPathChanged -= OnModPathChange;
_modManager.ModDiscoveryFinished -= Reload;
_modManager.ModDataChanged -= OnDataChange;
_modManager.ModPathChanged -= OnModPathChange;
_modManager.ModDiscoveryFinished -= Reload;
_communicator.ModDataChanged.Event -= OnDataChange;
}
public struct ImportDate : ISortMode<Mod>

View file

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

View file

@ -45,6 +45,13 @@ public class CommunicatorService : IDisposable
/// </list> </summary>
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()
{
CollectionChange.Dispose();
@ -52,5 +59,6 @@ public class CommunicatorService : IDisposable
ModMetaChange.Dispose();
CreatingCharacterBase.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>
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>
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>
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>
public string ModMetaPath(string modDirectory)

View file

@ -267,7 +267,7 @@ public class ItemSwapTab : IDisposable, ITab
private void CreateMod()
{
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);
_modManager.AddMod(newDir);
if (!_swapData.WriteMod(_modManager.Last(),

View file

@ -150,7 +150,7 @@ public partial class ModEditWindow : Window, IDisposable
_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 string _materialSuffixFrom = string.Empty;

View file

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

View file

@ -42,7 +42,7 @@ public class ModPanelDescriptionTab : ITab
out var editedTag);
_tutorial.OpenTutorial(BasicTutorialSteps.Tags);
if (tagIdx >= 0)
_modManager.ChangeLocalTag(_selector.Selected!.Index, tagIdx, editedTag);
_modManager.DataEditor.ChangeLocalTag(_selector.Selected!, tagIdx, editedTag);
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.",

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,
out var editedTag);
if (tagIdx >= 0)
_modManager.ChangeModTag(_mod.Index, tagIdx, editedTag);
_modManager.DataEditor.ChangeModTag(_mod, tagIdx, editedTag);
UiHelpers.DefaultLineSpace();
AddOptionGroup.Draw(_modManager, _mod);
@ -172,18 +172,18 @@ public class ModPanelEditTab : ITab
private void EditRegularMeta()
{
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))
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,
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,
UiHelpers.InputTextWidth.X))
_modManager.ChangeModWebsite(_mod.Index, newWebsite);
_modManager.DataEditor.ChangeModWebsite(_mod, newWebsite);
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));
ImGui.SameLine();
var fileExists = File.Exists(_mod.MetaFile.FullName);
var fileExists = File.Exists(_modManager.DataEditor.MetaFile(_mod));
var tt = fileExists
? "Open the metadata json file in the text editor of your choice."
: "The metadata json file does not exist.";
if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.FileExport.ToIconString()}##metaFile", UiHelpers.IconButtonSize, tt,
!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>
@ -349,7 +349,7 @@ public class ModPanelEditTab : ITab
switch (_newDescriptionIdx)
{
case Input.Description:
modManager.ChangeModDescription(_mod.Index, _newDescription);
modManager.DataEditor.ChangeModDescription(_mod, _newDescription);
break;
case >= 0:
if (_newDescriptionOptionIdx < 0)

View file

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

View file

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