mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +01:00
Fix handling of weird TTMP files.
This commit is contained in:
parent
4a9b08de98
commit
c8293c9a6b
1 changed files with 369 additions and 363 deletions
|
|
@ -4,7 +4,6 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Plugin;
|
||||
using ICSharpCode.SharpZipLib.Zip;
|
||||
using Newtonsoft.Json;
|
||||
using Penumbra.GameData.Util;
|
||||
|
|
@ -14,408 +13,415 @@ using Penumbra.Structs;
|
|||
using Penumbra.Util;
|
||||
using FileMode = System.IO.FileMode;
|
||||
|
||||
namespace Penumbra.Importer
|
||||
namespace Penumbra.Importer;
|
||||
|
||||
internal class TexToolsImport
|
||||
{
|
||||
internal class TexToolsImport
|
||||
private readonly DirectoryInfo _outDirectory;
|
||||
|
||||
private const string TempFileName = "textools-import";
|
||||
private readonly string _resolvedTempFilePath;
|
||||
|
||||
public DirectoryInfo? ExtractedDirectory { get; private set; }
|
||||
|
||||
public ImporterState State { get; private set; }
|
||||
|
||||
public long TotalProgress { get; private set; }
|
||||
public long CurrentProgress { get; private set; }
|
||||
|
||||
public float Progress
|
||||
{
|
||||
private readonly DirectoryInfo _outDirectory;
|
||||
|
||||
private const string TempFileName = "textools-import";
|
||||
private readonly string _resolvedTempFilePath;
|
||||
|
||||
public DirectoryInfo? ExtractedDirectory { get; private set; }
|
||||
|
||||
public ImporterState State { get; private set; }
|
||||
|
||||
public long TotalProgress { get; private set; }
|
||||
public long CurrentProgress { get; private set; }
|
||||
|
||||
public float Progress
|
||||
get
|
||||
{
|
||||
get
|
||||
if( CurrentProgress != 0 )
|
||||
{
|
||||
if( CurrentProgress != 0 )
|
||||
{
|
||||
// ReSharper disable twice RedundantCast
|
||||
return ( float )CurrentProgress / ( float )TotalProgress;
|
||||
}
|
||||
// ReSharper disable twice RedundantCast
|
||||
return ( float )CurrentProgress / ( float )TotalProgress;
|
||||
}
|
||||
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public string? CurrentModPack { get; private set; }
|
||||
|
||||
public TexToolsImport( DirectoryInfo outDirectory )
|
||||
{
|
||||
_outDirectory = outDirectory;
|
||||
_resolvedTempFilePath = Path.Combine( _outDirectory.FullName, TempFileName );
|
||||
}
|
||||
|
||||
private static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName )
|
||||
=> new(Path.Combine( baseDir.FullName, optionName.ReplaceBadXivSymbols() ));
|
||||
|
||||
public DirectoryInfo ImportModPack( FileInfo modPackFile )
|
||||
{
|
||||
CurrentModPack = modPackFile.Name;
|
||||
|
||||
var dir = VerifyVersionAndImport( modPackFile );
|
||||
|
||||
State = ImporterState.Done;
|
||||
return dir;
|
||||
}
|
||||
|
||||
private void WriteZipEntryToTempFile( Stream s )
|
||||
{
|
||||
var fs = new FileStream( _resolvedTempFilePath, FileMode.Create );
|
||||
s.CopyTo( fs );
|
||||
fs.Close();
|
||||
}
|
||||
|
||||
// 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 ];
|
||||
|
||||
if( entry.Name.Contains( fileName ) )
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
public string? CurrentModPack { get; private set; }
|
||||
return null;
|
||||
}
|
||||
|
||||
public TexToolsImport( DirectoryInfo outDirectory )
|
||||
private PenumbraSqPackStream GetMagicSqPackDeleterStream( ZipFile file, string entryName )
|
||||
{
|
||||
State = ImporterState.WritingPackToDisk;
|
||||
|
||||
// write shitty zip garbage to disk
|
||||
var entry = FindZipEntry( file, entryName );
|
||||
if( entry == null )
|
||||
{
|
||||
_outDirectory = outDirectory;
|
||||
_resolvedTempFilePath = Path.Combine( _outDirectory.FullName, TempFileName );
|
||||
throw new FileNotFoundException( $"ZIP does not contain a file named {entryName}." );
|
||||
}
|
||||
|
||||
private static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName )
|
||||
=> new( Path.Combine( baseDir.FullName, optionName.ReplaceBadXivSymbols() ) );
|
||||
using var s = file.GetInputStream( entry );
|
||||
|
||||
public DirectoryInfo ImportModPack( FileInfo modPackFile )
|
||||
WriteZipEntryToTempFile( s );
|
||||
|
||||
var fs = new FileStream( _resolvedTempFilePath, FileMode.Open );
|
||||
return new MagicTempFileStreamManagerAndDeleter( fs );
|
||||
}
|
||||
|
||||
private DirectoryInfo VerifyVersionAndImport( FileInfo modPackFile )
|
||||
{
|
||||
using var zfs = modPackFile.OpenRead();
|
||||
using var extractedModPack = new ZipFile( zfs );
|
||||
|
||||
var mpl = FindZipEntry( extractedModPack, "TTMPL.mpl" );
|
||||
if( mpl == null )
|
||||
{
|
||||
CurrentModPack = modPackFile.Name;
|
||||
|
||||
var dir = VerifyVersionAndImport( modPackFile );
|
||||
|
||||
State = ImporterState.Done;
|
||||
return dir;
|
||||
throw new FileNotFoundException( "ZIP does not contain a TTMPL.mpl file." );
|
||||
}
|
||||
|
||||
private void WriteZipEntryToTempFile( Stream s )
|
||||
var modRaw = GetStringFromZipEntry( extractedModPack, mpl, Encoding.UTF8 );
|
||||
|
||||
// At least a better validation than going by the extension.
|
||||
if( modRaw.Contains( "\"TTMPVersion\":" ) )
|
||||
{
|
||||
var fs = new FileStream( _resolvedTempFilePath, FileMode.Create );
|
||||
s.CopyTo( fs );
|
||||
fs.Close();
|
||||
if( modPackFile.Extension != ".ttmp2" )
|
||||
{
|
||||
PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V2 TTMP, but has the wrong extension." );
|
||||
}
|
||||
|
||||
return ImportV2ModPack( 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 )
|
||||
if( modPackFile.Extension != ".ttmp" )
|
||||
{
|
||||
for( var i = 0; i < file.Count; i++ )
|
||||
{
|
||||
var entry = file[ i ];
|
||||
|
||||
if( entry.Name.Contains( fileName ) )
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V1 TTMP, but has the wrong extension." );
|
||||
}
|
||||
|
||||
private PenumbraSqPackStream GetMagicSqPackDeleterStream( ZipFile file, string entryName )
|
||||
return ImportV1ModPack( modPackFile, extractedModPack, modRaw );
|
||||
}
|
||||
|
||||
private DirectoryInfo ImportV1ModPack( FileInfo modPackFile, ZipFile extractedModPack, string modRaw )
|
||||
{
|
||||
PluginLog.Log( " -> Importing V1 ModPack" );
|
||||
|
||||
var modListRaw = modRaw.Split(
|
||||
new[] { "\r\n", "\r", "\n" },
|
||||
StringSplitOptions.None
|
||||
);
|
||||
|
||||
var modList = modListRaw.Select( JsonConvert.DeserializeObject< SimpleMod > );
|
||||
|
||||
// Create a new ModMeta from the TTMP modlist info
|
||||
var modMeta = new ModMeta
|
||||
{
|
||||
State = ImporterState.WritingPackToDisk;
|
||||
Author = "Unknown",
|
||||
Name = modPackFile.Name,
|
||||
Description = "Mod imported from TexTools mod pack",
|
||||
};
|
||||
|
||||
// write shitty zip garbage to disk
|
||||
var entry = FindZipEntry( file, entryName );
|
||||
if( entry == null )
|
||||
{
|
||||
throw new FileNotFoundException( $"ZIP does not contain a file named {entryName}." );
|
||||
}
|
||||
// Open the mod data file from the modpack as a SqPackStream
|
||||
using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" );
|
||||
|
||||
using var s = file.GetInputStream( entry );
|
||||
ExtractedDirectory = CreateModFolder( _outDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) );
|
||||
|
||||
WriteZipEntryToTempFile( s );
|
||||
File.WriteAllText(
|
||||
Path.Combine( ExtractedDirectory.FullName, "meta.json" ),
|
||||
JsonConvert.SerializeObject( modMeta )
|
||||
);
|
||||
|
||||
var fs = new FileStream( _resolvedTempFilePath, FileMode.Open );
|
||||
return new MagicTempFileStreamManagerAndDeleter( fs );
|
||||
ExtractSimpleModList( ExtractedDirectory, modList, modData );
|
||||
|
||||
return ExtractedDirectory;
|
||||
}
|
||||
|
||||
private DirectoryInfo ImportV2ModPack( FileInfo _, ZipFile extractedModPack, string modRaw )
|
||||
{
|
||||
var modList = JsonConvert.DeserializeObject<SimpleModPack>( modRaw );
|
||||
|
||||
if( modList.TTMPVersion?.EndsWith( "s" ) ?? false )
|
||||
{
|
||||
return ImportSimpleV2ModPack( extractedModPack, modList );
|
||||
}
|
||||
|
||||
private DirectoryInfo VerifyVersionAndImport( FileInfo modPackFile )
|
||||
if( modList.TTMPVersion?.EndsWith( "w" ) ?? false )
|
||||
{
|
||||
using var zfs = modPackFile.OpenRead();
|
||||
using var extractedModPack = new ZipFile( zfs );
|
||||
|
||||
var mpl = FindZipEntry( extractedModPack, "TTMPL.mpl" );
|
||||
if( mpl == null )
|
||||
{
|
||||
throw new FileNotFoundException( "ZIP does not contain a TTMPL.mpl file." );
|
||||
}
|
||||
|
||||
var modRaw = GetStringFromZipEntry( extractedModPack, mpl, Encoding.UTF8 );
|
||||
|
||||
// At least a better validation than going by the extension.
|
||||
if( modRaw.Contains( "\"TTMPVersion\":" ) )
|
||||
{
|
||||
if( modPackFile.Extension != ".ttmp2" )
|
||||
{
|
||||
PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V2 TTMP, but has the wrong extension." );
|
||||
}
|
||||
|
||||
return ImportV2ModPack( modPackFile, extractedModPack, modRaw );
|
||||
}
|
||||
else
|
||||
{
|
||||
if( modPackFile.Extension != ".ttmp" )
|
||||
{
|
||||
PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V1 TTMP, but has the wrong extension." );
|
||||
}
|
||||
|
||||
return ImportV1ModPack( modPackFile, extractedModPack, modRaw );
|
||||
}
|
||||
return ImportExtendedV2ModPack( extractedModPack, modRaw );
|
||||
}
|
||||
|
||||
private DirectoryInfo ImportV1ModPack( FileInfo modPackFile, ZipFile extractedModPack, string modRaw )
|
||||
try
|
||||
{
|
||||
PluginLog.Log( " -> Importing V1 ModPack" );
|
||||
|
||||
var modListRaw = modRaw.Split(
|
||||
new[] { "\r\n", "\r", "\n" },
|
||||
StringSplitOptions.None
|
||||
);
|
||||
|
||||
var modList = modListRaw.Select( JsonConvert.DeserializeObject< SimpleMod > );
|
||||
|
||||
// Create a new ModMeta from the TTMP modlist info
|
||||
var modMeta = new ModMeta
|
||||
{
|
||||
Author = "Unknown",
|
||||
Name = modPackFile.Name,
|
||||
Description = "Mod imported from TexTools mod pack",
|
||||
};
|
||||
|
||||
// Open the mod data file from the modpack as a SqPackStream
|
||||
using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" );
|
||||
|
||||
ExtractedDirectory = CreateModFolder( _outDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) );
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine( ExtractedDirectory.FullName, "meta.json" ),
|
||||
JsonConvert.SerializeObject( modMeta )
|
||||
);
|
||||
|
||||
ExtractSimpleModList( ExtractedDirectory, modList, modData );
|
||||
|
||||
return ExtractedDirectory;
|
||||
PluginLog.Warning( $"Unknown TTMPVersion {modList.TTMPVersion ?? "NULL"} given, trying to export as simple Modpack." );
|
||||
return ImportSimpleV2ModPack( extractedModPack, modList );
|
||||
}
|
||||
|
||||
private DirectoryInfo ImportV2ModPack( FileInfo modPackFile, ZipFile extractedModPack, string modRaw )
|
||||
catch( Exception e1 )
|
||||
{
|
||||
var modList = JsonConvert.DeserializeObject< SimpleModPack >( modRaw );
|
||||
|
||||
if( modList?.TTMPVersion == null )
|
||||
{
|
||||
PluginLog.Error( "Could not extract V2 Modpack. No version given." );
|
||||
return new DirectoryInfo( "" );
|
||||
}
|
||||
|
||||
if( modList.TTMPVersion.EndsWith( "s" ) )
|
||||
{
|
||||
return ImportSimpleV2ModPack( extractedModPack, modList );
|
||||
}
|
||||
|
||||
if( modList.TTMPVersion.EndsWith( "w" ) )
|
||||
PluginLog.Warning( $"Exporting as simple Modpack failed with following error, retrying as extended Modpack:\n{e1}" );
|
||||
try
|
||||
{
|
||||
return ImportExtendedV2ModPack( extractedModPack, modRaw );
|
||||
}
|
||||
|
||||
return new DirectoryInfo( "" );
|
||||
}
|
||||
|
||||
public static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName )
|
||||
{
|
||||
var name = Path.GetFileName( modListName );
|
||||
if( !name.Any() )
|
||||
catch( Exception e2 )
|
||||
{
|
||||
name = "_";
|
||||
throw new IOException( "Exporting as extended Modpack failed, too. Version unsupported or file defect.", e2 );
|
||||
}
|
||||
|
||||
var newModFolderBase = NewOptionDirectory( outDirectory, name );
|
||||
var newModFolder = newModFolderBase;
|
||||
var i = 2;
|
||||
while( newModFolder.Exists && i < 12 )
|
||||
{
|
||||
newModFolder = new DirectoryInfo( newModFolderBase.FullName + $" ({i++})" );
|
||||
}
|
||||
|
||||
if( newModFolder.Exists )
|
||||
{
|
||||
throw new IOException( "Could not create mod folder: too many folders of the same name exist." );
|
||||
}
|
||||
|
||||
newModFolder.Create();
|
||||
return newModFolder;
|
||||
}
|
||||
|
||||
private DirectoryInfo ImportSimpleV2ModPack( ZipFile extractedModPack, SimpleModPack modList )
|
||||
{
|
||||
PluginLog.Log( " -> Importing Simple V2 ModPack" );
|
||||
|
||||
// Create a new ModMeta from the TTMP modlist info
|
||||
var modMeta = new ModMeta
|
||||
{
|
||||
Author = modList.Author ?? "Unknown",
|
||||
Name = modList.Name ?? "New Mod",
|
||||
Description = string.IsNullOrEmpty( modList.Description )
|
||||
? "Mod imported from TexTools mod pack"
|
||||
: modList.Description!,
|
||||
};
|
||||
|
||||
// Open the mod data file from the modpack as a SqPackStream
|
||||
using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" );
|
||||
|
||||
ExtractedDirectory = CreateModFolder( _outDirectory, modList.Name ?? "New Mod" );
|
||||
|
||||
File.WriteAllText( Path.Combine( ExtractedDirectory.FullName, "meta.json" ),
|
||||
JsonConvert.SerializeObject( modMeta ) );
|
||||
|
||||
ExtractSimpleModList( ExtractedDirectory, modList.SimpleModsList ?? Enumerable.Empty< SimpleMod >(), modData );
|
||||
return ExtractedDirectory;
|
||||
}
|
||||
|
||||
private DirectoryInfo ImportExtendedV2ModPack( ZipFile extractedModPack, string modRaw )
|
||||
{
|
||||
PluginLog.Log( " -> Importing Extended V2 ModPack" );
|
||||
|
||||
var modList = JsonConvert.DeserializeObject< ExtendedModPack >( modRaw );
|
||||
|
||||
// Create a new ModMeta from the TTMP modlist info
|
||||
var modMeta = new ModMeta
|
||||
{
|
||||
Author = modList.Author ?? "Unknown",
|
||||
Name = modList.Name ?? "New Mod",
|
||||
Description = string.IsNullOrEmpty( modList.Description )
|
||||
? "Mod imported from TexTools mod pack"
|
||||
: modList.Description ?? "",
|
||||
Version = modList.Version ?? "",
|
||||
};
|
||||
|
||||
// Open the mod data file from the modpack as a SqPackStream
|
||||
using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" );
|
||||
|
||||
ExtractedDirectory = CreateModFolder( _outDirectory, modList.Name ?? "New Mod" );
|
||||
|
||||
if( modList.SimpleModsList != null )
|
||||
{
|
||||
ExtractSimpleModList( ExtractedDirectory, modList.SimpleModsList, modData );
|
||||
}
|
||||
|
||||
if( modList.ModPackPages == null )
|
||||
{
|
||||
return ExtractedDirectory;
|
||||
}
|
||||
|
||||
// Iterate through all pages
|
||||
foreach( var page in modList.ModPackPages )
|
||||
{
|
||||
if( page.ModGroups == null )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach( var group in page.ModGroups.Where( group => group.GroupName != null && group.OptionList != null ) )
|
||||
{
|
||||
var groupFolder = NewOptionDirectory( ExtractedDirectory, group.GroupName! );
|
||||
if( groupFolder.Exists )
|
||||
{
|
||||
groupFolder = new DirectoryInfo( groupFolder.FullName + $" ({page.PageIndex})" );
|
||||
group.GroupName += $" ({page.PageIndex})";
|
||||
}
|
||||
|
||||
foreach( var option in group.OptionList!.Where( option => option.Name != null && option.ModsJsons != null ) )
|
||||
{
|
||||
var optionFolder = NewOptionDirectory( groupFolder, option.Name! );
|
||||
ExtractSimpleModList( optionFolder, option.ModsJsons!, modData );
|
||||
}
|
||||
|
||||
AddMeta( ExtractedDirectory, groupFolder, group, modMeta );
|
||||
}
|
||||
}
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine( ExtractedDirectory.FullName, "meta.json" ),
|
||||
JsonConvert.SerializeObject( modMeta, Formatting.Indented )
|
||||
);
|
||||
return ExtractedDirectory;
|
||||
}
|
||||
|
||||
private static void AddMeta( DirectoryInfo baseFolder, DirectoryInfo groupFolder, ModGroup group, ModMeta meta )
|
||||
{
|
||||
var inf = new OptionGroup
|
||||
{
|
||||
SelectionType = group.SelectionType,
|
||||
GroupName = group.GroupName!,
|
||||
Options = new List< Option >(),
|
||||
};
|
||||
foreach( var opt in group.OptionList! )
|
||||
{
|
||||
var option = new Option
|
||||
{
|
||||
OptionName = opt.Name!,
|
||||
OptionDesc = string.IsNullOrEmpty( opt.Description ) ? "" : opt.Description!,
|
||||
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(),
|
||||
};
|
||||
var optDir = NewOptionDirectory( groupFolder, opt.Name! );
|
||||
if( optDir.Exists )
|
||||
{
|
||||
foreach( var file in optDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
|
||||
{
|
||||
option.AddFile( new RelPath( file, baseFolder ), new GamePath( file, optDir ) );
|
||||
}
|
||||
}
|
||||
|
||||
inf.Options.Add( option );
|
||||
}
|
||||
|
||||
meta.Groups.Add( inf.GroupName, inf );
|
||||
}
|
||||
|
||||
private void ImportMetaModPack( FileInfo file )
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private void ExtractSimpleModList( DirectoryInfo outDirectory, IEnumerable< SimpleMod > mods, PenumbraSqPackStream dataStream )
|
||||
{
|
||||
State = ImporterState.ExtractingModFiles;
|
||||
|
||||
// haha allocation go brr
|
||||
var wtf = mods.ToList();
|
||||
|
||||
TotalProgress += wtf.LongCount();
|
||||
|
||||
// Extract each SimpleMod into the new mod folder
|
||||
foreach( var simpleMod in wtf.Where( m => m != null ) )
|
||||
{
|
||||
ExtractMod( outDirectory, simpleMod, dataStream );
|
||||
CurrentProgress++;
|
||||
}
|
||||
}
|
||||
|
||||
private void ExtractMod( DirectoryInfo outDirectory, SimpleMod mod, PenumbraSqPackStream dataStream )
|
||||
{
|
||||
PluginLog.Log( " -> Extracting {0} at {1}", mod.FullPath!, mod.ModOffset.ToString( "X" ) );
|
||||
|
||||
try
|
||||
{
|
||||
var data = dataStream.ReadFile< PenumbraSqPackStream.PenumbraFileResource >( mod.ModOffset );
|
||||
|
||||
var extractedFile = new FileInfo( Path.Combine( outDirectory.FullName, mod.FullPath! ) );
|
||||
extractedFile.Directory?.Create();
|
||||
|
||||
if( extractedFile.FullName.EndsWith( "mdl" ) )
|
||||
{
|
||||
ProcessMdl( data.Data );
|
||||
}
|
||||
|
||||
File.WriteAllBytes( extractedFile.FullName, data.Data );
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
PluginLog.LogError( ex, "Could not extract mod." );
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessMdl( byte[] mdl )
|
||||
{
|
||||
// Model file header LOD num
|
||||
mdl[ 64 ] = 1;
|
||||
|
||||
// Model header LOD num
|
||||
var stackSize = BitConverter.ToUInt32( mdl, 4 );
|
||||
var runtimeBegin = stackSize + 0x44;
|
||||
var stringsLengthOffset = runtimeBegin + 4;
|
||||
var stringsLength = BitConverter.ToUInt32( mdl, ( int )stringsLengthOffset );
|
||||
var modelHeaderStart = stringsLengthOffset + stringsLength + 4;
|
||||
var modelHeaderLodOffset = 22;
|
||||
mdl[ modelHeaderStart + modelHeaderLodOffset ] = 1;
|
||||
}
|
||||
|
||||
private static Stream GetStreamFromZipEntry( ZipFile file, ZipEntry entry )
|
||||
=> file.GetInputStream( entry );
|
||||
|
||||
private static string GetStringFromZipEntry( ZipFile file, ZipEntry entry, Encoding encoding )
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
using var s = GetStreamFromZipEntry( file, entry );
|
||||
s.CopyTo( ms );
|
||||
return encoding.GetString( ms.ToArray() );
|
||||
}
|
||||
}
|
||||
|
||||
public static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName )
|
||||
{
|
||||
var name = Path.GetFileName( modListName );
|
||||
if( !name.Any() )
|
||||
{
|
||||
name = "_";
|
||||
}
|
||||
|
||||
var newModFolderBase = NewOptionDirectory( outDirectory, name );
|
||||
var newModFolder = newModFolderBase;
|
||||
var i = 2;
|
||||
while( newModFolder.Exists && i < 12 )
|
||||
{
|
||||
newModFolder = new DirectoryInfo( newModFolderBase.FullName + $" ({i++})" );
|
||||
}
|
||||
|
||||
if( newModFolder.Exists )
|
||||
{
|
||||
throw new IOException( "Could not create mod folder: too many folders of the same name exist." );
|
||||
}
|
||||
|
||||
newModFolder.Create();
|
||||
return newModFolder;
|
||||
}
|
||||
|
||||
private DirectoryInfo ImportSimpleV2ModPack( ZipFile extractedModPack, SimpleModPack modList )
|
||||
{
|
||||
PluginLog.Log( " -> Importing Simple V2 ModPack" );
|
||||
|
||||
// Create a new ModMeta from the TTMP modlist info
|
||||
var modMeta = new ModMeta
|
||||
{
|
||||
Author = modList.Author ?? "Unknown",
|
||||
Name = modList.Name ?? "New Mod",
|
||||
Description = string.IsNullOrEmpty( modList.Description )
|
||||
? "Mod imported from TexTools mod pack"
|
||||
: modList.Description!,
|
||||
};
|
||||
|
||||
// Open the mod data file from the modpack as a SqPackStream
|
||||
using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" );
|
||||
|
||||
ExtractedDirectory = CreateModFolder( _outDirectory, modList.Name ?? "New Mod" );
|
||||
|
||||
File.WriteAllText( Path.Combine( ExtractedDirectory.FullName, "meta.json" ),
|
||||
JsonConvert.SerializeObject( modMeta ) );
|
||||
|
||||
ExtractSimpleModList( ExtractedDirectory, modList.SimpleModsList ?? Enumerable.Empty< SimpleMod >(), modData );
|
||||
return ExtractedDirectory;
|
||||
}
|
||||
|
||||
private DirectoryInfo ImportExtendedV2ModPack( ZipFile extractedModPack, string modRaw )
|
||||
{
|
||||
PluginLog.Log( " -> Importing Extended V2 ModPack" );
|
||||
|
||||
var modList = JsonConvert.DeserializeObject< ExtendedModPack >( modRaw );
|
||||
|
||||
// Create a new ModMeta from the TTMP modlist info
|
||||
var modMeta = new ModMeta
|
||||
{
|
||||
Author = modList.Author ?? "Unknown",
|
||||
Name = modList.Name ?? "New Mod",
|
||||
Description = string.IsNullOrEmpty( modList.Description )
|
||||
? "Mod imported from TexTools mod pack"
|
||||
: modList.Description ?? "",
|
||||
Version = modList.Version ?? "",
|
||||
};
|
||||
|
||||
// Open the mod data file from the modpack as a SqPackStream
|
||||
using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" );
|
||||
|
||||
ExtractedDirectory = CreateModFolder( _outDirectory, modList.Name ?? "New Mod" );
|
||||
|
||||
if( modList.SimpleModsList != null )
|
||||
{
|
||||
ExtractSimpleModList( ExtractedDirectory, modList.SimpleModsList, modData );
|
||||
}
|
||||
|
||||
if( modList.ModPackPages == null )
|
||||
{
|
||||
return ExtractedDirectory;
|
||||
}
|
||||
|
||||
// Iterate through all pages
|
||||
foreach( var page in modList.ModPackPages )
|
||||
{
|
||||
if( page.ModGroups == null )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach( var group in page.ModGroups.Where( group => group.GroupName != null && group.OptionList != null ) )
|
||||
{
|
||||
var groupFolder = NewOptionDirectory( ExtractedDirectory, group.GroupName! );
|
||||
if( groupFolder.Exists )
|
||||
{
|
||||
groupFolder = new DirectoryInfo( groupFolder.FullName + $" ({page.PageIndex})" );
|
||||
group.GroupName += $" ({page.PageIndex})";
|
||||
}
|
||||
|
||||
foreach( var option in group.OptionList!.Where( option => option.Name != null && option.ModsJsons != null ) )
|
||||
{
|
||||
var optionFolder = NewOptionDirectory( groupFolder, option.Name! );
|
||||
ExtractSimpleModList( optionFolder, option.ModsJsons!, modData );
|
||||
}
|
||||
|
||||
AddMeta( ExtractedDirectory, groupFolder, group, modMeta );
|
||||
}
|
||||
}
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine( ExtractedDirectory.FullName, "meta.json" ),
|
||||
JsonConvert.SerializeObject( modMeta, Formatting.Indented )
|
||||
);
|
||||
return ExtractedDirectory;
|
||||
}
|
||||
|
||||
private static void AddMeta( DirectoryInfo baseFolder, DirectoryInfo groupFolder, ModGroup group, ModMeta meta )
|
||||
{
|
||||
var inf = new OptionGroup
|
||||
{
|
||||
SelectionType = group.SelectionType,
|
||||
GroupName = group.GroupName!,
|
||||
Options = new List< Option >(),
|
||||
};
|
||||
foreach( var opt in group.OptionList! )
|
||||
{
|
||||
var option = new Option
|
||||
{
|
||||
OptionName = opt.Name!,
|
||||
OptionDesc = string.IsNullOrEmpty( opt.Description ) ? "" : opt.Description!,
|
||||
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(),
|
||||
};
|
||||
var optDir = NewOptionDirectory( groupFolder, opt.Name! );
|
||||
if( optDir.Exists )
|
||||
{
|
||||
foreach( var file in optDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
|
||||
{
|
||||
option.AddFile( new RelPath( file, baseFolder ), new GamePath( file, optDir ) );
|
||||
}
|
||||
}
|
||||
|
||||
inf.Options.Add( option );
|
||||
}
|
||||
|
||||
meta.Groups.Add( inf.GroupName, inf );
|
||||
}
|
||||
|
||||
private void ImportMetaModPack( FileInfo file )
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private void ExtractSimpleModList( DirectoryInfo outDirectory, IEnumerable< SimpleMod > mods, PenumbraSqPackStream dataStream )
|
||||
{
|
||||
State = ImporterState.ExtractingModFiles;
|
||||
|
||||
// haha allocation go brr
|
||||
var wtf = mods.ToList();
|
||||
|
||||
TotalProgress += wtf.LongCount();
|
||||
|
||||
// Extract each SimpleMod into the new mod folder
|
||||
foreach( var simpleMod in wtf.Where( m => m != null ) )
|
||||
{
|
||||
ExtractMod( outDirectory, simpleMod, dataStream );
|
||||
CurrentProgress++;
|
||||
}
|
||||
}
|
||||
|
||||
private void ExtractMod( DirectoryInfo outDirectory, SimpleMod mod, PenumbraSqPackStream dataStream )
|
||||
{
|
||||
PluginLog.Log( " -> Extracting {0} at {1}", mod.FullPath!, mod.ModOffset.ToString( "X" ) );
|
||||
|
||||
try
|
||||
{
|
||||
var data = dataStream.ReadFile< PenumbraSqPackStream.PenumbraFileResource >( mod.ModOffset );
|
||||
|
||||
var extractedFile = new FileInfo( Path.Combine( outDirectory.FullName, mod.FullPath! ) );
|
||||
extractedFile.Directory?.Create();
|
||||
|
||||
if( extractedFile.FullName.EndsWith( "mdl" ) )
|
||||
{
|
||||
ProcessMdl( data.Data );
|
||||
}
|
||||
|
||||
File.WriteAllBytes( extractedFile.FullName, data.Data );
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
PluginLog.LogError( ex, "Could not extract mod." );
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessMdl( byte[] mdl )
|
||||
{
|
||||
// Model file header LOD num
|
||||
mdl[ 64 ] = 1;
|
||||
|
||||
// Model header LOD num
|
||||
var stackSize = BitConverter.ToUInt32( mdl, 4 );
|
||||
var runtimeBegin = stackSize + 0x44;
|
||||
var stringsLengthOffset = runtimeBegin + 4;
|
||||
var stringsLength = BitConverter.ToUInt32( mdl, ( int )stringsLengthOffset );
|
||||
var modelHeaderStart = stringsLengthOffset + stringsLength + 4;
|
||||
var modelHeaderLodOffset = 22;
|
||||
mdl[ modelHeaderStart + modelHeaderLodOffset ] = 1;
|
||||
}
|
||||
|
||||
private static Stream GetStreamFromZipEntry( ZipFile file, ZipEntry entry )
|
||||
=> file.GetInputStream( entry );
|
||||
|
||||
private static string GetStringFromZipEntry( ZipFile file, ZipEntry entry, Encoding encoding )
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
using var s = GetStreamFromZipEntry( file, entry );
|
||||
s.CopyTo( ms );
|
||||
return encoding.GetString( ms.ToArray() );
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue