diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs
index 73cb9029..3dcf5d47 100644
--- a/Penumbra/Import/TexToolsImport.cs
+++ b/Penumbra/Import/TexToolsImport.cs
@@ -1,14 +1,16 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Logging;
-using ICSharpCode.SharpZipLib.Zip;
using Newtonsoft.Json;
using Penumbra.Mods;
using FileMode = System.IO.FileMode;
+using ZipArchive = SharpCompress.Archives.Zip.ZipArchive;
+using ZipArchiveEntry = SharpCompress.Archives.Zip.ZipArchiveEntry;
namespace Penumbra.Import;
@@ -119,8 +121,13 @@ public partial class TexToolsImporter : IDisposable
// Puts out warnings if extension does not correspond to data.
private DirectoryInfo VerifyVersionAndImport( FileInfo modPackFile )
{
+ if( modPackFile.Extension is ".zip" or ".7z" or ".rar" )
+ {
+ return HandleRegularArchive( modPackFile );
+ }
+
using var zfs = modPackFile.OpenRead();
- using var extractedModPack = new ZipFile( zfs );
+ using var extractedModPack = ZipArchive.Open( zfs );
var mpl = FindZipEntry( extractedModPack, "TTMPL.mpl" );
if( mpl == null )
@@ -128,7 +135,7 @@ public partial class TexToolsImporter : IDisposable
throw new FileNotFoundException( "ZIP does not contain a TTMPL.mpl file." );
}
- var modRaw = GetStringFromZipEntry( extractedModPack, mpl, Encoding.UTF8 );
+ var modRaw = GetStringFromZipEntry( mpl, Encoding.UTF8 );
// At least a better validation than going by the extension.
if( modRaw.Contains( "\"TTMPVersion\":" ) )
@@ -149,30 +156,14 @@ public partial class TexToolsImporter : IDisposable
return ImportV1ModPack( modPackFile, extractedModPack, modRaw );
}
-
// You can in no way rely on any file paths in TTMPs so we need to just do this, sorry
- private static ZipEntry? FindZipEntry( ZipFile file, string fileName )
- {
- for( var i = 0; i < file.Count; i++ )
- {
- var entry = file[ i ];
+ private static ZipArchiveEntry? FindZipEntry( ZipArchive file, string fileName )
+ => file.Entries.FirstOrDefault( e => !e.IsDirectory && e.Key.Contains( fileName ) );
- if( entry.Name.Contains( fileName ) )
- {
- return entry;
- }
- }
-
- return null;
- }
-
- private static Stream GetStreamFromZipEntry( ZipFile file, ZipEntry entry )
- => file.GetInputStream( entry );
-
- private static string GetStringFromZipEntry( ZipFile file, ZipEntry entry, Encoding encoding )
+ private static string GetStringFromZipEntry( ZipArchiveEntry entry, Encoding encoding )
{
using var ms = new MemoryStream();
- using var s = GetStreamFromZipEntry( file, entry );
+ using var s = entry.OpenEntryStream();
s.CopyTo( ms );
return encoding.GetString( ms.ToArray() );
}
@@ -191,7 +182,7 @@ public partial class TexToolsImporter : IDisposable
_tmpFileStream = null;
}
- private StreamDisposer GetSqPackStreamStream( ZipFile file, string entryName )
+ private StreamDisposer GetSqPackStreamStream( ZipArchive file, string entryName )
{
State = ImporterState.WritingPackToDisk;
@@ -202,7 +193,7 @@ public partial class TexToolsImporter : IDisposable
throw new FileNotFoundException( $"ZIP does not contain a file named {entryName}." );
}
- using var s = file.GetInputStream( entry );
+ using var s = entry.OpenEntryStream();
WriteZipEntryToTempFile( s );
diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs
new file mode 100644
index 00000000..0f461c79
--- /dev/null
+++ b/Penumbra/Import/TexToolsImporter.Archives.cs
@@ -0,0 +1,123 @@
+using System;
+using System.IO;
+using System.Linq;
+using Dalamud.Logging;
+using Dalamud.Utility;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using OtterGui.Filesystem;
+using Penumbra.Mods;
+using SharpCompress.Archives;
+using SharpCompress.Common;
+
+namespace Penumbra.Import;
+
+public partial class TexToolsImporter
+{
+ // Extract regular compressed archives that are folders containing penumbra-formatted mods.
+ // The mod has to either contain a meta.json at top level, or one folder deep.
+ // If the meta.json is one folder deep, all other files have to be in the same folder.
+ // The extracted folder gets its name either from that one top-level folder or from the mod name.
+ // All data is extracted without manipulation of the files or metadata.
+ private DirectoryInfo HandleRegularArchive( FileInfo modPackFile )
+ {
+ using var zfs = modPackFile.OpenRead();
+ using var archive = ArchiveFactory.Open( zfs );
+
+ var baseName = FindArchiveModMeta( archive, out var leadDir );
+ _currentOptionIdx = 0;
+ _currentNumOptions = 1;
+ _currentModName = modPackFile.Name;
+ _currentGroupName = string.Empty;
+ _currentOptionName = DefaultTexToolsData.Name;
+ _currentNumFiles = archive.Entries.Count( e => !e.IsDirectory );
+ PluginLog.Log( $" -> Importing {archive.Type} Archive." );
+
+ _currentModDirectory = Mod.CreateModFolder( _baseDirectory, baseName );
+ var options = new ExtractionOptions()
+ {
+ ExtractFullPath = true,
+ Overwrite = true,
+ };
+
+ State = ImporterState.ExtractingModFiles;
+ _currentFileIdx = 0;
+ foreach( var entry in archive.Entries )
+ {
+ _token.ThrowIfCancellationRequested();
+
+ if( entry.IsDirectory )
+ {
+ ++_currentFileIdx;
+ continue;
+ }
+
+ PluginLog.Log( " -> Extracting {0}", entry.Key );
+ entry.WriteToDirectory( _currentModDirectory.FullName, options );
+
+ ++_currentFileIdx;
+ }
+
+ if( leadDir )
+ {
+ _token.ThrowIfCancellationRequested();
+ var oldName = _currentModDirectory.FullName;
+ var tmpName = oldName + "__tmp";
+ Directory.Move( oldName, tmpName );
+ Directory.Move( Path.Combine( tmpName, baseName ), oldName );
+ Directory.Delete( tmpName );
+ _currentModDirectory = new DirectoryInfo( oldName );
+ }
+
+ return _currentModDirectory;
+ }
+
+ // Search the archive for the meta.json file which needs to exist.
+ private static string FindArchiveModMeta( IArchive archive, out bool leadDir )
+ {
+ var entry = archive.Entries.FirstOrDefault( e => !e.IsDirectory && e.Key.EndsWith( "meta.json" ) );
+ // None found.
+ if( entry == null )
+ {
+ throw new Exception( "Invalid mod archive: No meta.json contained." );
+ }
+
+ var ret = string.Empty;
+ leadDir = false;
+
+ // If the file is not at top-level.
+ if( entry.Key != "meta.json" )
+ {
+ leadDir = true;
+ var directory = Path.GetDirectoryName( entry.Key );
+ // Should not happen.
+ if( directory.IsNullOrEmpty() )
+ {
+ throw new Exception( "Invalid mod archive: Unknown error fetching meta.json." );
+ }
+
+ ret = directory;
+ // Check that all other files are also contained in the top-level directory.
+ if( ret.IndexOfAny( new[] { '/', '\\' } ) >= 0
+ || !archive.Entries.All( e => e.Key.StartsWith( ret ) && ( e.Key.Length == ret.Length || e.Key[ ret.Length ] is '/' or '\\' ) ) )
+ {
+ throw new Exception(
+ "Invalid mod archive: meta.json in wrong location. It needs to be either at root or one directory deep, in which all other files must be nested too." );
+ }
+ }
+
+ // Check that the mod has a valid name in the meta.json file.
+ using var e = entry.OpenEntryStream();
+ using var t = new StreamReader( e );
+ using var j = new JsonTextReader( t );
+ var obj = JObject.Load( j );
+ var name = obj[ nameof( Mod.Name ) ]?.Value< string >().RemoveInvalidPathSymbols() ?? string.Empty;
+ if( name.Length == 0 )
+ {
+ throw new Exception( "Invalid mod archive: mod meta has no name." );
+ }
+
+ // Use either the top-level directory as the mods base name, or the (fixed for path) name in the json.
+ return ret.Length == 0 ? name : ret;
+ }
+}
\ No newline at end of file
diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs
index f15053cf..574efb1f 100644
--- a/Penumbra/Import/TexToolsImporter.ModPack.cs
+++ b/Penumbra/Import/TexToolsImporter.ModPack.cs
@@ -4,10 +4,10 @@ using System.IO;
using System.Linq;
using System.Text;
using Dalamud.Logging;
-using ICSharpCode.SharpZipLib.Zip;
using Newtonsoft.Json;
using Penumbra.Mods;
using Penumbra.Util;
+using SharpCompress.Archives.Zip;
namespace Penumbra.Import;
@@ -16,7 +16,7 @@ public partial class TexToolsImporter
private DirectoryInfo? _currentModDirectory;
// Version 1 mod packs are a simple collection of files without much information.
- private DirectoryInfo ImportV1ModPack( FileInfo modPackFile, ZipFile extractedModPack, string modRaw )
+ private DirectoryInfo ImportV1ModPack( FileInfo modPackFile, ZipArchive extractedModPack, string modRaw )
{
_currentOptionIdx = 0;
_currentNumOptions = 1;
@@ -46,7 +46,7 @@ public partial class TexToolsImporter
}
// Version 2 mod packs can either be simple or extended, import accordingly.
- private DirectoryInfo ImportV2ModPack( FileInfo _, ZipFile extractedModPack, string modRaw )
+ private DirectoryInfo ImportV2ModPack( FileInfo _, ZipArchive extractedModPack, string modRaw )
{
var modList = JsonConvert.DeserializeObject< SimpleModPack >( modRaw, JsonSettings )!;
@@ -80,7 +80,7 @@ public partial class TexToolsImporter
}
// Simple V2 mod packs are basically the same as V1 mod packs.
- private DirectoryInfo ImportSimpleV2ModPack( ZipFile extractedModPack, SimpleModPack modList )
+ private DirectoryInfo ImportSimpleV2ModPack( ZipArchive extractedModPack, SimpleModPack modList )
{
_currentOptionIdx = 0;
_currentNumOptions = 1;
@@ -125,7 +125,7 @@ public partial class TexToolsImporter
}
// Extended V2 mod packs contain multiple options that need to be handled separately.
- private DirectoryInfo ImportExtendedV2ModPack( ZipFile extractedModPack, string modRaw )
+ private DirectoryInfo ImportExtendedV2ModPack( ZipArchive extractedModPack, string modRaw )
{
_currentOptionIdx = 0;
PluginLog.Log( " -> Importing Extended V2 ModPack" );
diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj
index 26e8d088..f6a4be96 100644
--- a/Penumbra/Penumbra.csproj
+++ b/Penumbra/Penumbra.csproj
@@ -55,8 +55,8 @@
-
+
diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs
index 57580c05..eced30cb 100644
--- a/Penumbra/UI/Classes/ModFileSystemSelector.cs
+++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs
@@ -212,7 +212,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
: Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null;
_hasSetFolder = true;
- _fileManager.OpenFileDialog( "Import Mod Pack", "TexTools Mod Packs{.ttmp,.ttmp2}", ( s, f ) =>
+ _fileManager.OpenFileDialog( "Import Mod Pack", "Mod Packs{.ttmp,.ttmp2,.zip,.7z,.rar},TexTools Mod Packs{.ttmp,.ttmp2},Archives{.zip,.7z,.rar}", ( s, f ) =>
{
if( s )
{