mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-13 12:14:17 +01:00
Allow Penumbra to import regular archives for penumbra mods.
This commit is contained in:
parent
ee48c7803c
commit
7305ad41ac
5 changed files with 146 additions and 32 deletions
|
|
@ -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 );
|
||||
|
||||
|
|
|
|||
123
Penumbra/Import/TexToolsImporter.Archives.cs
Normal file
123
Penumbra/Import/TexToolsImporter.Archives.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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" );
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 )
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue