Move TexTools around.

This commit is contained in:
Ottermandias 2023-03-23 21:43:40 +01:00
parent f38a252295
commit 174e640c45
19 changed files with 212 additions and 228 deletions

View file

@ -9,7 +9,7 @@ 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; using Penumbra.Import.Structs;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI; using Penumbra.UI;

View file

@ -1,122 +0,0 @@
using Penumbra.GameData.Enums;
using System.Text.RegularExpressions;
using Penumbra.GameData;
namespace Penumbra.Import;
// Obtain information what type of object is manipulated
// by the given .meta file from TexTools, using its name.
public class MetaFileInfo
{
private const string Pt = @"(?'PrimaryType'[a-z]*)"; // language=regex
private const string Pp = @"(?'PrimaryPrefix'[a-z])"; // language=regex
private const string Pi = @"(?'PrimaryId'\d{4})"; // language=regex
private const string Pir = @"\k'PrimaryId'"; // language=regex
private const string St = @"(?'SecondaryType'[a-z]*)"; // language=regex
private const string Sp = @"(?'SecondaryPrefix'[a-z])"; // language=regex
private const string Si = @"(?'SecondaryId'\d{4})"; // language=regex
private const string File = @"\k'PrimaryPrefix'\k'PrimaryId'(\k'SecondaryPrefix'\k'SecondaryId')?"; // language=regex
private const string Slot = @"(_(?'Slot'[a-z]{3}))?"; // language=regex
private const string Ext = @"\.meta";
// These are the valid regexes for .meta files that we are able to support at the moment.
private static readonly Regex HousingMeta = new($"bgcommon/hou/{Pt}/general/{Pi}/{Pir}{Ext}", RegexOptions.Compiled);
private static readonly Regex CharaMeta = new($"chara/{Pt}/{Pp}{Pi}(/obj/{St}/{Sp}{Si})?/{File}{Slot}{Ext}", RegexOptions.Compiled);
public readonly ObjectType PrimaryType;
public readonly BodySlot SecondaryType;
public readonly ushort PrimaryId;
public readonly ushort SecondaryId;
public readonly EquipSlot EquipSlot = EquipSlot.Unknown;
public readonly CustomizationType CustomizationType = CustomizationType.Unknown;
private static bool ValidType( ObjectType type )
{
return type switch
{
ObjectType.Accessory => true,
ObjectType.Character => true,
ObjectType.Equipment => true,
ObjectType.DemiHuman => true,
ObjectType.Housing => true,
ObjectType.Monster => true,
ObjectType.Weapon => true,
ObjectType.Icon => false,
ObjectType.Font => false,
ObjectType.Interface => false,
ObjectType.LoadingScreen => false,
ObjectType.Map => false,
ObjectType.Vfx => false,
ObjectType.Unknown => false,
ObjectType.World => false,
_ => false,
};
}
public MetaFileInfo( IGamePathParser parser, string fileName )
{
// Set the primary type from the gamePath start.
PrimaryType = parser.PathToObjectType( fileName );
PrimaryId = 0;
SecondaryType = BodySlot.Unknown;
SecondaryId = 0;
// Not all types of objects can have valid meta data manipulation.
if( !ValidType( PrimaryType ) )
{
PrimaryType = ObjectType.Unknown;
return;
}
// Housing files have a separate regex that just contains the primary id.
if( PrimaryType == ObjectType.Housing )
{
var housingMatch = HousingMeta.Match( fileName );
if( housingMatch.Success )
{
PrimaryId = ushort.Parse( housingMatch.Groups[ "PrimaryId" ].Value );
}
return;
}
// Non-housing is in chara/.
var match = CharaMeta.Match( fileName );
if( !match.Success )
{
return;
}
// The primary ID has to be available for every object.
PrimaryId = ushort.Parse( match.Groups[ "PrimaryId" ].Value );
// Depending on slot, we can set equip slot or customization type.
if( match.Groups[ "Slot" ].Success )
{
switch( PrimaryType )
{
case ObjectType.Equipment:
case ObjectType.Accessory:
if( Names.SuffixToEquipSlot.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpSlot ) )
{
EquipSlot = tmpSlot;
}
break;
case ObjectType.Character:
if( Names.SuffixToCustomizationType.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpCustom ) )
{
CustomizationType = tmpCustom;
}
break;
}
}
// Secondary type and secondary id are for weapons and demihumans.
if( match.Groups[ "SecondaryType" ].Success
&& Names.StringToBodySlot.TryGetValue( match.Groups[ "SecondaryType" ].Value, out SecondaryType ) )
{
SecondaryId = ushort.Parse( match.Groups[ "SecondaryId" ].Value );
}
}
}

View file

@ -1,4 +1,4 @@
namespace Penumbra.Import; namespace Penumbra.Import.Structs;
public enum ImporterState public enum ImporterState
{ {

View file

@ -0,0 +1,104 @@
using Penumbra.GameData.Enums;
using System.Text.RegularExpressions;
using Penumbra.GameData;
namespace Penumbra.Import.Structs;
/// <summary>
/// Obtain information what type of object is manipulated
/// by the given .meta file from TexTools, using its name.
/// </summary>
public partial struct MetaFileInfo
{
// These are the valid regexes for .meta files that we are able to support at the moment.
[GeneratedRegex(@"bgcommon/hou/(?'Type1'[a-z]*)/general/(?'Id1'\d{4})/\k'Id1'\.meta",
RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.NonBacktracking)]
private static partial Regex HousingMeta();
[GeneratedRegex(
@"chara/(?'Type1'[a-z]*)/(?'Pre1'[a-z])(?'Id1'\d{4})(/obj/(?'Type2'[a-z]*)/(?'Pre2'[a-z])(?'Id2'\d{4}))?/\k'Pre1'\k'Id1'(\k'Pre2'\k'Id2')?(_(?'Slot'[a-z]{3}))?\\.meta",
RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.NonBacktracking)]
private static partial Regex CharaMeta();
public readonly ObjectType PrimaryType;
public readonly BodySlot SecondaryType;
public readonly ushort PrimaryId;
public readonly ushort SecondaryId;
public readonly EquipSlot EquipSlot = EquipSlot.Unknown;
public readonly CustomizationType CustomizationType = CustomizationType.Unknown;
private static bool ValidType(ObjectType type)
=> type switch
{
ObjectType.Accessory => true,
ObjectType.Character => true,
ObjectType.Equipment => true,
ObjectType.DemiHuman => true,
ObjectType.Housing => true,
ObjectType.Monster => true,
ObjectType.Weapon => true,
ObjectType.Icon => false,
ObjectType.Font => false,
ObjectType.Interface => false,
ObjectType.LoadingScreen => false,
ObjectType.Map => false,
ObjectType.Vfx => false,
ObjectType.Unknown => false,
ObjectType.World => false,
_ => false,
};
public MetaFileInfo(IGamePathParser parser, string fileName)
{
// Set the primary type from the gamePath start.
PrimaryType = parser.PathToObjectType(fileName);
PrimaryId = 0;
SecondaryType = BodySlot.Unknown;
SecondaryId = 0;
// Not all types of objects can have valid meta data manipulation.
if (!ValidType(PrimaryType))
{
PrimaryType = ObjectType.Unknown;
return;
}
// Housing files have a separate regex that just contains the primary id.
if (PrimaryType == ObjectType.Housing)
{
var housingMatch = HousingMeta().Match(fileName);
if (housingMatch.Success)
PrimaryId = ushort.Parse(housingMatch.Groups["Id1"].Value);
return;
}
// Non-housing is in chara/.
var match = CharaMeta().Match(fileName);
if (!match.Success)
return;
// The primary ID has to be available for every object.
PrimaryId = ushort.Parse(match.Groups["Id1"].Value);
// Depending on slot, we can set equip slot or customization type.
if (match.Groups["Slot"].Success)
switch (PrimaryType)
{
case ObjectType.Equipment:
case ObjectType.Accessory:
if (Names.SuffixToEquipSlot.TryGetValue(match.Groups["Slot"].Value, out var tmpSlot))
EquipSlot = tmpSlot;
break;
case ObjectType.Character:
if (Names.SuffixToCustomizationType.TryGetValue(match.Groups["Slot"].Value, out var tmpCustom))
CustomizationType = tmpCustom;
break;
}
// Secondary type and secondary id are for weapons and demihumans.
if (match.Groups["Type2"].Success && Names.StringToBodySlot.TryGetValue(match.Groups["Type2"].Value, out SecondaryType))
SecondaryId = ushort.Parse(match.Groups["Id2"].Value);
}
}

View file

@ -2,15 +2,15 @@ using Penumbra.Util;
using System; using System;
using System.IO; using System.IO;
namespace Penumbra.Import; namespace Penumbra.Import.Structs;
// Create an automatically disposing SqPack stream. // Create an automatically disposing SqPack stream.
public class StreamDisposer : PenumbraSqPackStream, IDisposable public class StreamDisposer : PenumbraSqPackStream, IDisposable
{ {
private readonly FileStream _fileStream; private readonly FileStream _fileStream;
public StreamDisposer( FileStream stream ) public StreamDisposer(FileStream stream)
: base( stream ) : base(stream)
=> _fileStream = stream; => _fileStream = stream;
public new void Dispose() public new void Dispose()
@ -20,6 +20,6 @@ public class StreamDisposer : PenumbraSqPackStream, IDisposable
base.Dispose(); base.Dispose();
_fileStream.Dispose(); _fileStream.Dispose();
File.Delete( filePath ); File.Delete(filePath);
} }
} }

View file

@ -1,7 +1,7 @@
using System; using System;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
namespace Penumbra.Import; namespace Penumbra.Import.Structs;
internal static class DefaultTexToolsData internal static class DefaultTexToolsData
{ {
@ -27,7 +27,7 @@ internal class SimpleMod
internal class ModPackPage internal class ModPackPage
{ {
public int PageIndex = 0; public int PageIndex = 0;
public ModGroup[] ModGroups = Array.Empty< ModGroup >(); public ModGroup[] ModGroups = Array.Empty<ModGroup>();
} }
[Serializable] [Serializable]
@ -35,7 +35,7 @@ internal class ModGroup
{ {
public string GroupName = string.Empty; public string GroupName = string.Empty;
public GroupType SelectionType = GroupType.Single; public GroupType SelectionType = GroupType.Single;
public OptionList[] OptionList = Array.Empty< OptionList >(); public OptionList[] OptionList = Array.Empty<OptionList>();
public string Description = string.Empty; public string Description = string.Empty;
} }
@ -45,7 +45,7 @@ internal class OptionList
public string Name = string.Empty; public string Name = string.Empty;
public string Description = string.Empty; public string Description = string.Empty;
public string ImagePath = string.Empty; public string ImagePath = string.Empty;
public SimpleMod[] ModsJsons = Array.Empty< SimpleMod >(); public SimpleMod[] ModsJsons = Array.Empty<SimpleMod>();
public string GroupName = string.Empty; public string GroupName = string.Empty;
public GroupType SelectionType = GroupType.Single; public GroupType SelectionType = GroupType.Single;
public bool IsChecked = false; public bool IsChecked = false;
@ -60,8 +60,8 @@ internal class ExtendedModPack
public string Version = string.Empty; public string Version = string.Empty;
public string Description = DefaultTexToolsData.Description; public string Description = DefaultTexToolsData.Description;
public string Url = string.Empty; public string Url = string.Empty;
public ModPackPage[] ModPackPages = Array.Empty< ModPackPage >(); public ModPackPage[] ModPackPages = Array.Empty<ModPackPage>();
public SimpleMod[] SimpleModsList = Array.Empty< SimpleMod >(); public SimpleMod[] SimpleModsList = Array.Empty<SimpleMod>();
} }
[Serializable] [Serializable]
@ -73,5 +73,5 @@ internal class SimpleModPack
public string Version = string.Empty; public string Version = string.Empty;
public string Description = DefaultTexToolsData.Description; public string Description = DefaultTexToolsData.Description;
public string Url = string.Empty; public string Url = string.Empty;
public SimpleMod[] SimpleModsList = Array.Empty< SimpleMod >(); public SimpleMod[] SimpleModsList = Array.Empty<SimpleMod>();
} }

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -6,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.Import.Structs;
using Penumbra.Mods; using Penumbra.Mods;
using FileMode = System.IO.FileMode; using FileMode = System.IO.FileMode;
using ZipArchive = SharpCompress.Archives.Zip.ZipArchive; using ZipArchive = SharpCompress.Archives.Zip.ZipArchive;

View file

@ -2,6 +2,7 @@ using Dalamud.Utility;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.Import.Structs;
using Penumbra.Mods; using Penumbra.Mods;
using SharpCompress.Archives; using SharpCompress.Archives;
using SharpCompress.Archives.Rar; using SharpCompress.Archives.Rar;

View file

@ -3,6 +3,7 @@ using System.Numerics;
using ImGuiNET; using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.Import.Structs;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
namespace Penumbra.Import; namespace Penumbra.Import;

View file

@ -4,6 +4,7 @@ using System.IO;
using System.Linq; using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Import.Structs;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Util; using Penumbra.Util;
using SharpCompress.Archives.Zip; using SharpCompress.Archives.Zip;

View file

@ -3,6 +3,7 @@ using System.IO;
using Lumina.Extensions; using Lumina.Extensions;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Import.Structs;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.Import.Structs;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
namespace Penumbra.Import; namespace Penumbra.Import;

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Numerics; using System.Numerics;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.ImGuiFileDialog;
using ImGuiNET; using ImGuiNET;
using ImGuiScene; using ImGuiScene;
using Lumina.Data.Files; using Lumina.Data.Files;

View file

@ -1,10 +1,10 @@
using System;
using System.IO;
using Lumina.Data.Files; using Lumina.Data.Files;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using System;
using System.IO;
namespace Penumbra.Import.Dds; namespace Penumbra.Import.Textures;
public static class TextureImporter public static class TextureImporter
{ {

View file

@ -82,7 +82,7 @@ public unsafe partial class CharacterUtility
ResetResourceInternal(); ResetResourceInternal();
} }
// Set the currently stored data of this resource to new values. /// <summary> Set the currently stored data of this resource to new values. </summary>
private void SetResourceInternal(nint data, int length) private void SetResourceInternal(nint data, int length)
{ {
if (!Ready) if (!Ready)
@ -92,7 +92,7 @@ public unsafe partial class CharacterUtility
resource->SetData(data, length); resource->SetData(data, length);
} }
// Reset the currently stored data of this resource to its default values. /// <summary> Reset the currently stored data of this resource to its default values. </summary>
private void ResetResourceInternal() private void ResetResourceInternal()
=> SetResourceInternal(_defaultResourceData, _defaultResourceSize); => SetResourceInternal(_defaultResourceData, _defaultResourceSize);

View file

@ -5,141 +5,136 @@ using System.Threading.Tasks;
namespace Penumbra.Mods; namespace Penumbra.Mods;
// Utility to create and apply a zipped backup of a mod. /// <summary> Utility to create and apply a zipped backup of a mod. </summary>
public class ModBackup public class ModBackup
{ {
public static bool CreatingBackup { get; private set; } public static bool CreatingBackup { get; private set; }
private readonly Mod.Manager _modManager;
private readonly Mod _mod; private readonly Mod _mod;
public readonly string Name; public readonly string Name;
public readonly bool Exists; public readonly bool Exists;
public ModBackup( Mod.Manager modManager, Mod mod ) public ModBackup(Mod.Manager modManager, Mod mod)
{ {
_modManager = modManager;
_mod = mod; _mod = mod;
Name = Path.Combine( modManager.ExportDirectory.FullName, _mod.ModPath.Name ) + ".pmp"; Name = Path.Combine(_modManager.ExportDirectory.FullName, _mod.ModPath.Name) + ".pmp";
Exists = File.Exists( Name ); Exists = File.Exists(Name);
} }
// Migrate file extensions. /// <summary> Migrate file extensions. </summary>
public static void MigrateZipToPmp( Mod.Manager manager ) public static void MigrateZipToPmp(Mod.Manager manager)
{ {
foreach( var mod in manager ) foreach (var mod in manager)
{ {
var pmpName = mod.ModPath + ".pmp"; var pmpName = mod.ModPath + ".pmp";
var zipName = mod.ModPath + ".zip"; var zipName = mod.ModPath + ".zip";
if( File.Exists( zipName ) ) if (!File.Exists(zipName))
{ continue;
try try
{ {
if( !File.Exists( pmpName ) ) if (!File.Exists(pmpName))
{ File.Move(zipName, pmpName);
File.Move( zipName, pmpName );
}
else else
{ File.Delete(zipName);
File.Delete( zipName );
}
Penumbra.Log.Information( $"Migrated mod export from {zipName} to {pmpName}." ); Penumbra.Log.Information($"Migrated mod export from {zipName} to {pmpName}.");
} }
catch( Exception e ) catch (Exception e)
{ {
Penumbra.Log.Warning( $"Could not migrate mod export of {mod.ModPath} from .pmp to .zip:\n{e}" ); Penumbra.Log.Warning($"Could not migrate mod export of {mod.ModPath} from .pmp to .zip:\n{e}");
}
} }
} }
} }
// Move and/or rename an exported mod. /// <summary>
// This object is unusable afterwards. /// Move and/or rename an exported mod.
public void Move( string? newBasePath = null, string? newName = null ) /// This object is unusable afterwards.
{ /// </summary>
if( CreatingBackup || !Exists ) public void Move(string? newBasePath = null, string? newName = null)
{ {
if (CreatingBackup || !Exists)
return; return;
}
try try
{ {
newBasePath ??= Path.GetDirectoryName( Name ) ?? string.Empty; newBasePath ??= Path.GetDirectoryName(Name) ?? string.Empty;
newName = newName == null ? Path.GetFileName( Name ) : newName + ".pmp"; newName = newName == null ? Path.GetFileName(Name) : newName + ".pmp";
var newPath = Path.Combine( newBasePath, newName ); var newPath = Path.Combine(newBasePath, newName);
File.Move( Name, newPath ); File.Move(Name, newPath);
} }
catch( Exception e ) catch (Exception e)
{ {
Penumbra.Log.Warning( $"Could not move mod export file {Name}:\n{e}" ); Penumbra.Log.Warning($"Could not move mod export file {Name}:\n{e}");
} }
} }
// Create a backup zip without blocking the main thread. /// <summary> Create a backup zip without blocking the main thread. </summary>
public async void CreateAsync() public async void CreateAsync()
{ {
if( CreatingBackup ) if (CreatingBackup)
{
return; return;
}
CreatingBackup = true; CreatingBackup = true;
await Task.Run( Create ); await Task.Run(Create);
CreatingBackup = false; CreatingBackup = false;
} }
/// <summary> Create a backup. Overwrites pre-existing backups. </summary>
// Create a backup. Overwrites pre-existing backups.
private void Create() private void Create()
{ {
try try
{ {
Delete(); Delete();
ZipFile.CreateFromDirectory( _mod.ModPath.FullName, Name, CompressionLevel.Optimal, false ); ZipFile.CreateFromDirectory(_mod.ModPath.FullName, Name, CompressionLevel.Optimal, false);
Penumbra.Log.Debug( $"Created export file {Name} from {_mod.ModPath.FullName}." ); Penumbra.Log.Debug($"Created export file {Name} from {_mod.ModPath.FullName}.");
} }
catch( Exception e ) catch (Exception e)
{ {
Penumbra.Log.Error( $"Could not export mod {_mod.Name} to \"{Name}\":\n{e}" ); Penumbra.Log.Error($"Could not export mod {_mod.Name} to \"{Name}\":\n{e}");
} }
} }
// Delete a pre-existing backup. /// <summary> Delete a pre-existing backup. </summary>
public void Delete() public void Delete()
{ {
if( !Exists ) if (!Exists)
{
return; return;
}
try try
{ {
File.Delete( Name ); File.Delete(Name);
Penumbra.Log.Debug( $"Deleted export file {Name}." ); Penumbra.Log.Debug($"Deleted export file {Name}.");
} }
catch( Exception e ) catch (Exception e)
{ {
Penumbra.Log.Error( $"Could not delete file \"{Name}\":\n{e}" ); Penumbra.Log.Error($"Could not delete file \"{Name}\":\n{e}");
} }
} }
// Restore a mod from a pre-existing backup. Does not check if the mod contained in the backup is even similar. /// <summary>
// Does an automatic reload after extraction. /// Restore a mod from a pre-existing backup. Does not check if the mod contained in the backup is even similar.
/// Does an automatic reload after extraction.
/// </summary>
public void Restore() public void Restore()
{ {
try try
{ {
if( Directory.Exists( _mod.ModPath.FullName ) ) if (Directory.Exists(_mod.ModPath.FullName))
{ {
Directory.Delete( _mod.ModPath.FullName, true ); Directory.Delete(_mod.ModPath.FullName, true);
Penumbra.Log.Debug( $"Deleted mod folder {_mod.ModPath.FullName}." ); Penumbra.Log.Debug($"Deleted mod folder {_mod.ModPath.FullName}.");
} }
ZipFile.ExtractToDirectory( Name, _mod.ModPath.FullName ); ZipFile.ExtractToDirectory(Name, _mod.ModPath.FullName);
Penumbra.Log.Debug( $"Extracted exported file {Name} to {_mod.ModPath.FullName}." ); Penumbra.Log.Debug($"Extracted exported file {Name} to {_mod.ModPath.FullName}.");
Penumbra.ModManager.ReloadMod( _mod.Index ); _modManager.ReloadMod(_mod.Index);
} }
catch( Exception e ) catch (Exception e)
{ {
Penumbra.Log.Error( $"Could not restore {_mod.Name} from export \"{Name}\":\n{e}" ); Penumbra.Log.Error($"Could not restore {_mod.Name} from export \"{Name}\":\n{e}");
} }
} }
} }

View file

@ -88,7 +88,7 @@ public class ModEditor : IDisposable
GroupIdx = -1; GroupIdx = -1;
OptionIdx = 0; OptionIdx = 0;
if (message) if (message)
global::Penumbra.Penumbra.Log.Error($"Loading invalid option {groupIdx} {optionIdx} for Mod {Mod?.Name ?? "Unknown"}."); Penumbra.Log.Error($"Loading invalid option {groupIdx} {optionIdx} for Mod {Mod?.Name ?? "Unknown"}.");
} }
public void Clear() public void Clear()

View file

@ -10,7 +10,7 @@ using Newtonsoft.Json.Linq;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Import; using Penumbra.Import.Structs;
using Penumbra.String.Classes; using Penumbra.String.Classes;
namespace Penumbra.Mods; namespace Penumbra.Mods;

View file

@ -15,6 +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.Mods; using Penumbra.Mods;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;