Allow Penumbra to import regular archives for penumbra mods.

This commit is contained in:
Ottermandias 2022-07-27 16:00:37 +02:00
parent ee48c7803c
commit 7305ad41ac
5 changed files with 146 additions and 32 deletions

View file

@ -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 );

View file

@ -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;
}
}

View file

@ -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" );

View file

@ -55,8 +55,8 @@
<ItemGroup>
<PackageReference Include="EmbedIO" Version="3.4.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="SharpZipLib" Version="1.3.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.2" />
<PackageReference Include="SharpCompress" Version="0.32.1" />
</ItemGroup>
<ItemGroup>

View file

@ -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 )
{