mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-14 12:44:19 +01:00
279 lines
No EOL
12 KiB
C#
279 lines
No EOL
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using Dalamud.Logging;
|
|
using Newtonsoft.Json;
|
|
using Penumbra.Mods;
|
|
using Penumbra.Util;
|
|
using SharpCompress.Archives.Zip;
|
|
|
|
namespace Penumbra.Import;
|
|
|
|
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, ZipArchive extractedModPack, string modRaw )
|
|
{
|
|
_currentOptionIdx = 0;
|
|
_currentNumOptions = 1;
|
|
_currentModName = modPackFile.Name.Length > 0 ? modPackFile.Name : DefaultTexToolsData.Name;
|
|
_currentGroupName = string.Empty;
|
|
_currentOptionName = DefaultTexToolsData.DefaultOption;
|
|
|
|
PluginLog.Log( " -> Importing V1 ModPack" );
|
|
|
|
var modListRaw = modRaw.Split(
|
|
new[] { "\r\n", "\r", "\n" },
|
|
StringSplitOptions.RemoveEmptyEntries
|
|
);
|
|
|
|
var modList = modListRaw.Select( m => JsonConvert.DeserializeObject< SimpleMod >( m, JsonSettings )! ).ToList();
|
|
|
|
_currentModDirectory = Mod.CreateModFolder( _baseDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) );
|
|
// Create a new ModMeta from the TTMP mod list info
|
|
Mod.CreateMeta( _currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null );
|
|
|
|
// Open the mod data file from the mod pack as a SqPackStream
|
|
_streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" );
|
|
ExtractSimpleModList( _currentModDirectory, modList );
|
|
Mod.CreateDefaultFiles( _currentModDirectory );
|
|
ResetStreamDisposer();
|
|
return _currentModDirectory;
|
|
}
|
|
|
|
// Version 2 mod packs can either be simple or extended, import accordingly.
|
|
private DirectoryInfo ImportV2ModPack( FileInfo _, ZipArchive extractedModPack, string modRaw )
|
|
{
|
|
var modList = JsonConvert.DeserializeObject< SimpleModPack >( modRaw, JsonSettings )!;
|
|
|
|
if( modList.TtmpVersion.EndsWith( "s" ) )
|
|
{
|
|
return ImportSimpleV2ModPack( extractedModPack, modList );
|
|
}
|
|
|
|
if( modList.TtmpVersion.EndsWith( "w" ) )
|
|
{
|
|
return ImportExtendedV2ModPack( extractedModPack, modRaw );
|
|
}
|
|
|
|
try
|
|
{
|
|
PluginLog.Warning( $"Unknown TTMPVersion <{modList.TtmpVersion}> given, trying to export as simple mod pack." );
|
|
return ImportSimpleV2ModPack( extractedModPack, modList );
|
|
}
|
|
catch( Exception e1 )
|
|
{
|
|
PluginLog.Warning( $"Exporting as simple mod pack failed with following error, retrying as extended mod pack:\n{e1}" );
|
|
try
|
|
{
|
|
return ImportExtendedV2ModPack( extractedModPack, modRaw );
|
|
}
|
|
catch( Exception e2 )
|
|
{
|
|
throw new IOException( "Exporting as extended mod pack failed, too. Version unsupported or file defect.", e2 );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Simple V2 mod packs are basically the same as V1 mod packs.
|
|
private DirectoryInfo ImportSimpleV2ModPack( ZipArchive extractedModPack, SimpleModPack modList )
|
|
{
|
|
_currentOptionIdx = 0;
|
|
_currentNumOptions = 1;
|
|
_currentModName = modList.Name;
|
|
_currentGroupName = string.Empty;
|
|
_currentOptionName = DefaultTexToolsData.DefaultOption;
|
|
PluginLog.Log( " -> Importing Simple V2 ModPack" );
|
|
|
|
_currentModDirectory = Mod.CreateModFolder( _baseDirectory, _currentModName );
|
|
Mod.CreateMeta( _currentModDirectory, _currentModName, modList.Author, string.IsNullOrEmpty( modList.Description )
|
|
? "Mod imported from TexTools mod pack"
|
|
: modList.Description, modList.Version, modList.Url );
|
|
|
|
// Open the mod data file from the mod pack as a SqPackStream
|
|
_streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" );
|
|
ExtractSimpleModList( _currentModDirectory, modList.SimpleModsList );
|
|
Mod.CreateDefaultFiles( _currentModDirectory );
|
|
ResetStreamDisposer();
|
|
return _currentModDirectory;
|
|
}
|
|
|
|
// Obtain the number of relevant options to extract.
|
|
private static int GetOptionCount( ExtendedModPack pack )
|
|
=> ( pack.SimpleModsList.Length > 0 ? 1 : 0 )
|
|
+ pack.ModPackPages
|
|
.Sum( page => page.ModGroups
|
|
.Where( g => g.GroupName.Length > 0 && g.OptionList.Length > 0 )
|
|
.Sum( group => group.OptionList
|
|
.Count( o => o.Name.Length > 0 && o.ModsJsons.Length > 0 )
|
|
+ ( group.OptionList.Any( o => o.Name.Length > 0 && o.ModsJsons.Length == 0 ) ? 1 : 0 ) ) );
|
|
|
|
private static string GetGroupName( string groupName, ISet< string > names )
|
|
{
|
|
var baseName = groupName;
|
|
var i = 2;
|
|
while( !names.Add( groupName ) )
|
|
{
|
|
groupName = $"{baseName} ({i++})";
|
|
}
|
|
|
|
return groupName;
|
|
}
|
|
|
|
// Extended V2 mod packs contain multiple options that need to be handled separately.
|
|
private DirectoryInfo ImportExtendedV2ModPack( ZipArchive extractedModPack, string modRaw )
|
|
{
|
|
_currentOptionIdx = 0;
|
|
PluginLog.Log( " -> Importing Extended V2 ModPack" );
|
|
|
|
var modList = JsonConvert.DeserializeObject< ExtendedModPack >( modRaw, JsonSettings )!;
|
|
_currentNumOptions = GetOptionCount( modList );
|
|
_currentModName = modList.Name;
|
|
|
|
_currentModDirectory = Mod.CreateModFolder( _baseDirectory, _currentModName );
|
|
Mod.CreateMeta( _currentModDirectory, _currentModName, modList.Author, modList.Description, modList.Version, modList.Url );
|
|
|
|
if( _currentNumOptions == 0 )
|
|
{
|
|
return _currentModDirectory;
|
|
}
|
|
|
|
// Open the mod data file from the mod pack as a SqPackStream
|
|
_streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" );
|
|
|
|
// It can contain a simple list, still.
|
|
if( modList.SimpleModsList.Length > 0 )
|
|
{
|
|
_currentGroupName = string.Empty;
|
|
_currentOptionName = "Default";
|
|
ExtractSimpleModList( _currentModDirectory, modList.SimpleModsList );
|
|
}
|
|
|
|
// Iterate through all pages
|
|
var options = new List< ISubMod >();
|
|
var groupPriority = 0;
|
|
var groupNames = new HashSet< string >();
|
|
foreach( var page in modList.ModPackPages )
|
|
{
|
|
foreach( var group in page.ModGroups.Where( group => group.GroupName.Length > 0 && group.OptionList.Length > 0 ) )
|
|
{
|
|
var allOptions = group.OptionList.Where( option => option.Name.Length > 0 && option.ModsJsons.Length > 0 ).ToList();
|
|
var (numGroups, maxOptions) = group.SelectionType == SelectType.Single
|
|
? ( 1, allOptions.Count )
|
|
: ( 1 + allOptions.Count / IModGroup.MaxMultiOptions, IModGroup.MaxMultiOptions );
|
|
_currentGroupName = GetGroupName( group.GroupName, groupNames );
|
|
|
|
var optionIdx = 0;
|
|
for( var groupId = 0; groupId < numGroups; ++groupId )
|
|
{
|
|
var name = numGroups == 1 ? _currentGroupName : $"{_currentGroupName}, Part {groupId + 1}";
|
|
options.Clear();
|
|
var description = new StringBuilder();
|
|
var groupFolder = Mod.NewSubFolderName( _currentModDirectory, name )
|
|
?? new DirectoryInfo( Path.Combine( _currentModDirectory.FullName,
|
|
numGroups == 1 ? $"Group {groupPriority + 1}" : $"Group {groupPriority + 1}, Part {groupId + 1}" ) );
|
|
|
|
for( var i = 0; i + optionIdx < allOptions.Count && i < maxOptions; ++i )
|
|
{
|
|
var option = allOptions[ i + optionIdx ];
|
|
_token.ThrowIfCancellationRequested();
|
|
_currentOptionName = option.Name;
|
|
var optionFolder = Mod.NewSubFolderName( groupFolder, option.Name )
|
|
?? new DirectoryInfo( Path.Combine( groupFolder.FullName, $"Option {i + optionIdx + 1}" ) );
|
|
ExtractSimpleModList( optionFolder, option.ModsJsons );
|
|
options.Add( Mod.CreateSubMod( _currentModDirectory, optionFolder, option ) );
|
|
description.Append( option.Description );
|
|
if( !string.IsNullOrEmpty( option.Description ) )
|
|
{
|
|
description.Append( '\n' );
|
|
}
|
|
|
|
++_currentOptionIdx;
|
|
}
|
|
|
|
optionIdx += maxOptions;
|
|
|
|
// Handle empty options for single select groups without creating a folder for them.
|
|
// We only want one of those at most, and it should usually be the first option.
|
|
if( group.SelectionType == SelectType.Single )
|
|
{
|
|
var empty = group.OptionList.FirstOrDefault( o => o.Name.Length > 0 && o.ModsJsons.Length == 0 );
|
|
if( empty != null )
|
|
{
|
|
_currentOptionName = empty.Name;
|
|
options.Insert( 0, Mod.CreateEmptySubMod( empty.Name ) );
|
|
}
|
|
}
|
|
|
|
Mod.CreateOptionGroup( _currentModDirectory, group.SelectionType, name, groupPriority, groupPriority,
|
|
description.ToString(), options );
|
|
++groupPriority;
|
|
}
|
|
}
|
|
}
|
|
|
|
ResetStreamDisposer();
|
|
Mod.CreateDefaultFiles( _currentModDirectory );
|
|
return _currentModDirectory;
|
|
}
|
|
|
|
private void ExtractSimpleModList( DirectoryInfo outDirectory, ICollection< SimpleMod > mods )
|
|
{
|
|
State = ImporterState.ExtractingModFiles;
|
|
|
|
_currentFileIdx = 0;
|
|
_currentNumFiles = mods.Count(m => m.FullPath.Length > 0);
|
|
|
|
// Extract each SimpleMod into the new mod folder
|
|
foreach( var simpleMod in mods.Where(m => m.FullPath.Length > 0 ) )
|
|
{
|
|
ExtractMod( outDirectory, simpleMod );
|
|
++_currentFileIdx;
|
|
}
|
|
}
|
|
|
|
private void ExtractMod( DirectoryInfo outDirectory, SimpleMod mod )
|
|
{
|
|
if( _streamDisposer is not PenumbraSqPackStream stream )
|
|
{
|
|
return;
|
|
}
|
|
|
|
PluginLog.Log( " -> Extracting {0} at {1}", mod.FullPath, mod.ModOffset.ToString( "X" ) );
|
|
|
|
_token.ThrowIfCancellationRequested();
|
|
var data = stream.ReadFile< PenumbraSqPackStream.PenumbraFileResource >( mod.ModOffset );
|
|
|
|
_currentFileName = mod.FullPath;
|
|
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 );
|
|
}
|
|
|
|
private static void ProcessMdl( byte[] mdl )
|
|
{
|
|
const int modelHeaderLodOffset = 22;
|
|
|
|
// 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;
|
|
mdl[ modelHeaderStart + modelHeaderLodOffset ] = 1;
|
|
}
|
|
} |