Skip to content

Commit

Permalink
Implemented new batch processing spell
Browse files Browse the repository at this point in the history
  • Loading branch information
fo76utils committed Oct 23, 2024
1 parent e7bde04 commit 247fa41
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 45 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
== CHANGELOG ==

* Added an experimental new spell for batch processing multiple NIF files, casting any from a selection of currently 7 spells. The files to be processed can be selected with a file dialog, and are overwritten with the modified data (it is recommended to back up the original NIFs).
* Converting Starfield geometry to external mesh files can now use a custom sub-directory under 'geometries'. This can be configured in the general settings under NIF, and if the name is not empty, exported mesh paths will be in the format 'geometries/SUBDIR/SHA1.mesh', and the full hash (40 characters) will be used as the base name of the file.
* The default startup NIF version has been changed from Oblivion to Skyrim: Special Edition.
* Fixed setting the NIF version in new windows from the startup defaults.
* Fixed issue reading version 22 Fallout 76 BGEM files due to unknown new fGlassBlurScaleFactor setting.
* Fixed exporting Starfield mesh files with no meshlet data.

#### NifSkope-2.0.dev9-20241017

Expand Down
2 changes: 1 addition & 1 deletion src/model/nifmodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,7 @@ public slots:
QList<int> rootLinks;

bool lockUpdates;
bool batchProcessingMode;
bool batchProcessingMode = false;

enum UpdateType
{
Expand Down
35 changes: 26 additions & 9 deletions src/nifskope.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1469,6 +1469,7 @@ bool NifSkope::batchProcessFiles(
dlg.setResult( QDialog::Accepted );
dlg.show();

NifModel * tmpNif = nullptr;
bool noErrors = true;
for ( qsizetype i = 0; i < n; i++ ) {
const QString & filePath = fileList[i];
Expand All @@ -1478,26 +1479,42 @@ bool NifSkope::batchProcessFiles(
if ( dlg.result() == QDialog::Rejected )
return false;

loadFile( filePath );
QString fileName( QDir::fromNativeSeparators( filePath ) );
tmpNif = new NifModel();
tmpNif->setBatchProcessingMode( true );
{
QFile f( fileName );
if ( !f.open( QIODeviceBase::ReadOnly ) )
throw FO76UtilsError( "error opening file" );
std::string tmp( fileName.toStdString() );
tmpNif->load( f, tmp.c_str() );
}

QCoreApplication::processEvents();
if ( dlg.result() == QDialog::Rejected )
return false;

nif->setBatchProcessingMode( true );
bool saveFlag = processFunc( nif, processFuncData );
nif->setBatchProcessingMode( false );
bool saveFlag = processFunc( tmpNif, processFuncData );
tmpNif->setBatchProcessingMode( false );

QCoreApplication::processEvents();
if ( dlg.result() == QDialog::Rejected )
return false;

if ( saveFlag ) {
saveFile( filePath );
QCoreApplication::processEvents();
if ( dlg.result() == QDialog::Rejected )
return false;
QFile f( fileName );
if ( !f.open( QIODeviceBase::WriteOnly ) )
throw FO76UtilsError( "error opening file" );
tmpNif->save( f );
}

delete tmpNif;
tmpNif = nullptr;
} catch ( std::exception & e ) {
nif->setBatchProcessingMode( false );
if ( tmpNif ) {
delete tmpNif;
tmpNif = nullptr;
}
if ( QMessageBox::critical( this, "NifSkope error",
QString( "Error processing '%1': %2. Continue?" ).arg( filePath ).arg( e.what() ),
QMessageBox::Yes | QMessageBox::No ) != QMessageBox::Yes ) {
Expand Down
100 changes: 74 additions & 26 deletions src/spells/fileextract.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ std::string spResourceFileExtract::getOutputDirectory( const NifModel * nif )
QString key = QString( "Spells//Extract File/Last File Path" );
QString dstPath( settings.value( key ).toString() );
if ( !( nif && nif->getBatchProcessingMode() ) ) {
QFileDialog dialog;
QFileDialog dialog( nullptr, "Select Export Data Path" );
dialog.setFileMode( QFileDialog::Directory );
if ( !dstPath.isEmpty() )
dialog.setDirectory( dstPath );
Expand Down Expand Up @@ -417,11 +417,12 @@ class spMeshFileExport final : public Spell
return ( item->name() == "BSGeometry" && ( nif->get<quint32>(item, "Flags") & 0x0200 ) != 0 );
}

bool processItem( NifModel * nif, NifItem * item, const std::string & outputDirectory );
bool processItem( NifModel * nif, NifItem * item, const std::string & outputDirectory, const QString & meshDir );
QModelIndex cast( NifModel * nif, const QModelIndex & index ) override final;
};

bool spMeshFileExport::processItem( NifModel * nif, NifItem * item, const std::string & outputDirectory )
bool spMeshFileExport::processItem(
NifModel * nif, NifItem * item, const std::string & outputDirectory, const QString & meshDir )
{
quint32 flags;
if ( !( item && item->name() == "BSGeometry" && ( (flags = nif->get<quint32>(item, "Flags")) & 0x0200 ) != 0 ) )
Expand All @@ -441,22 +442,28 @@ bool spMeshFileExport::processItem( NifModel * nif, NifItem * item, const std::s
continue;
haveMeshes = true;

QBuffer meshBuf;
meshBuf.open( QIODevice::WriteOnly );
QByteArray meshBuf;
{
NifOStream nifStream( nif, &meshBuf );
QBuffer tmpBuf( &meshBuf );
tmpBuf.open( QIODevice::WriteOnly );
NifOStream nifStream( nif, &tmpBuf );
nif->saveItem( nif->getItem( meshData, false ), nifStream );
}
if ( !( nif->get<quint32>( meshData, "Num Meshlets" ) | nif->get<quint32>( meshData, "Num Cull Data" ) ) )
meshBuf.chop( 8 ); // end of file after LODs if there are no meshlets

QCryptographicHash h( QCryptographicHash::Sha1 );
h.addData( meshBuf.data() );
h.addData( meshBuf );
meshPaths[l] = h.result().toHex();
meshPaths[l].insert( 20, QChar('\\') );
if ( meshDir.isEmpty() )
meshPaths[l].insert( 20, QChar('\\') );
else
meshPaths[l].insert( 0, meshDir );

std::string fullPath( outputDirectory );
fullPath += Game::GameManager::get_full_path( meshPaths[l], "geometries/", ".mesh" );
try {
spResourceFileExtract::writeFileWithPath( fullPath, meshBuf.data().data(), meshBuf.data().size() );
spResourceFileExtract::writeFileWithPath( fullPath, meshBuf.data(), meshBuf.size() );
} catch ( std::exception & e ) {
QMessageBox::critical( nullptr, "NifSkope error", QString("Error extracting file: %1" ).arg( e.what() ) );
}
Expand Down Expand Up @@ -491,19 +498,28 @@ QModelIndex spMeshFileExport::cast( NifModel * nif, const QModelIndex & index )
if ( outputDirectory.empty() )
return index;

QString meshDir;
{
QSettings settings;
meshDir = settings.value( "Settings/Nif/Mesh Export Dir", QString() ).toString().trimmed().toLower();
}
meshDir.replace( QChar('/'), QChar('\\') );
while ( meshDir.endsWith( QChar('\\') ) )
meshDir.chop( 1 );
while ( meshDir.startsWith( QChar('\\') ) )
meshDir.remove( 0, 1 );
if ( !meshDir.isEmpty() )
meshDir.append( QChar('\\') );

bool meshesConverted = false;
if ( item ) {
meshesConverted = processItem( nif, item, outputDirectory );
meshesConverted = processItem( nif, item, outputDirectory, meshDir );
} else {
for ( int b = 0; b < nif->getBlockCount(); b++ )
meshesConverted |= processItem( nif, nif->getBlockItem( qint32(b) ), outputDirectory );
}
if ( meshesConverted ) {
#ifdef QT_NO_DEBUG
if ( !nif->getBatchProcessingMode() )
#endif
Game::GameManager::close_resources();
meshesConverted |= processItem( nif, nif->getBlockItem( qint32(b) ), outputDirectory, meshDir );
}
if ( meshesConverted && !nif->getBatchProcessingMode() )
Game::GameManager::close_resources();

return index;
}
Expand Down Expand Up @@ -555,7 +571,10 @@ bool spMeshFileImport::processItem( NifModel * nif, NifItem * item )
if ( meshPath.isEmpty() )
continue;
if ( !nif->getResourceFile( meshData[l], meshPath, "geometries/", ".mesh" ) ) {
QMessageBox::critical( nullptr, "NifSkope error", QString("Failed to load mesh file '%1'" ).arg( meshPath ) );
if ( nif->getBatchProcessingMode() )
throw FO76UtilsError( "failed to load mesh file '%s'", meshPath.toStdString().c_str() );
else
QMessageBox::critical( nullptr, "NifSkope error", QString("Failed to load mesh file '%1'" ).arg( meshPath ) );
return false;
}
}
Expand Down Expand Up @@ -624,6 +643,7 @@ class spBatchProcessFiles final : public Spell
{
return QIcon();
}
bool constant() const override final { return true; }
bool instant() const override final { return true; }

bool isApplicable( [[maybe_unused]] const NifModel * nif, const QModelIndex & index ) override final
Expand All @@ -644,6 +664,36 @@ class spBatchProcessFiles final : public Spell
QModelIndex cast( NifModel * nif, const QModelIndex & index ) override final;
};

class spRemoveUnusedStrings
{
public:
static QModelIndex cast_Static( NifModel * nif, const QModelIndex & index );
};

class spSimplifySFMesh
{
public:
static QModelIndex cast_Static( NifModel * nif, const QModelIndex & index );
};

class spAddAllTangentSpaces
{
public:
static QModelIndex cast_Static( NifModel * nif, const QModelIndex & index );
};

class spGenerateMeshlets
{
public:
static QModelIndex cast_Static( NifModel * nif, const QModelIndex & index );
};

class spUpdateAllBounds
{
public:
static QModelIndex cast_Static( NifModel * nif, const QModelIndex & index );
};

bool spBatchProcessFiles::processFile( NifModel * nif, void * p )
{
int spellMask = *( reinterpret_cast< int * >( p ) );
Expand All @@ -655,27 +705,27 @@ bool spBatchProcessFiles::processFile( NifModel * nif, void * p )
}

if ( spellMask & spellFlagRemoveUnusedStrings ) {
// TODO
spRemoveUnusedStrings::cast_Static( nif, QModelIndex() );
fileChanged = true;
}

if ( ( spellMask & spellFlagLODGen ) && nif->getBSVersion() >= 170 ) {
// TODO
spSimplifySFMesh::cast_Static( nif, QModelIndex() );
fileChanged = true;
}

if ( spellMask & spellFlagTangentSpace ) {
// TODO
spAddAllTangentSpaces::cast_Static( nif, QModelIndex() );
fileChanged = true;
}

if ( ( spellMask & spellFlagMeshlets ) && nif->getBSVersion() >= 170 ) {
// TODO
spGenerateMeshlets::cast_Static( nif, QModelIndex() );
fileChanged = true;
}

if ( spellMask & spellFlagUpdateBounds ) {
// TODO
spUpdateAllBounds::cast_Static( nif, QModelIndex() );
fileChanged = true;
}

Expand All @@ -697,7 +747,7 @@ QModelIndex spBatchProcessFiles::cast( [[maybe_unused]] NifModel * nif, const QM
{
QDialog dlg;
QLabel * lb = new QLabel( &dlg );
lb->setText( "Warning: this spell closes the current model, and selected files will be overwritten" );
lb->setText( "Batch process multiple models, overwriting the original NIF files" );
QLabel * lb2 = new QLabel( "Select spells to be cast, in the order listed:", &dlg );
QCheckBox * checkInternalGeom = new QCheckBox( "Convert to Internal Geometry", &dlg );
QCheckBox * checkRemoveUnusedStrings = new QCheckBox( "Remove Unused Strings", &dlg );
Expand Down Expand Up @@ -765,10 +815,8 @@ QModelIndex spBatchProcessFiles::cast( [[maybe_unused]] NifModel * nif, const QM
NifSkope * w = dynamic_cast< NifSkope * >( nif->getWindow() );
if ( w ) {
w->batchProcessFiles( fileList, &processFile, &spellMask );
#ifdef QT_NO_DEBUG
if ( spellMask & spellFlagExternalGeom )
Game::GameManager::close_resources();
#endif
}

return index;
Expand Down
18 changes: 18 additions & 0 deletions src/spells/mesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1640,8 +1640,18 @@ class spUpdateAllBounds final : public Spell

return QModelIndex();
}

static QModelIndex cast_Static( NifModel * nif, const QModelIndex & index );
};

QModelIndex spUpdateAllBounds::cast_Static( NifModel * nif, const QModelIndex & index )
{
spUpdateAllBounds tmp;
if ( tmp.isApplicable( nif, index ) )
return tmp.cast( nif, index );
return index;
}

REGISTER_SPELL( spUpdateAllBounds )


Expand Down Expand Up @@ -1831,6 +1841,14 @@ QModelIndex spGenerateMeshlets::cast( NifModel * nif, const QModelIndex & index
return spUpdateBounds::cast_Starfield( nif, index );
}

QModelIndex spGenerateMeshlets::cast_Static( NifModel * nif, const QModelIndex & index )
{
spGenerateMeshlets tmp;
if ( tmp.isApplicable( nif, index ) )
return tmp.cast( nif, index );
return index;
}

REGISTER_SPELL( spGenerateMeshlets )


Expand Down
1 change: 1 addition & 0 deletions src/spells/mesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class spGenerateMeshlets final : public Spell
static void clearMeshlets( NifModel * nif, const QModelIndex & iMeshData );
static void updateMeshlets( NifModel * nif, const QPersistentModelIndex & iMeshData, const MeshFile & meshFile );
QModelIndex cast( NifModel * nif, const QModelIndex & index ) override final;
static QModelIndex cast_Static( NifModel * nif, const QModelIndex & index );
};

//! Removes unused vertices
Expand Down
23 changes: 17 additions & 6 deletions src/spells/optimize.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -573,16 +573,27 @@ class spRemoveUnusedStrings final : public Spell
for ( const auto & s : newStrings )
originalStrings.removeAll( s );

QString msg;
if ( originalStrings.size() )
msg = "Removed:\r\n" + QStringList::fromVector( originalStrings ).join( "\r\n" );
if ( !nif->getBatchProcessingMode() ) {
QString msg;
if ( originalStrings.size() )
msg = "Removed:\r\n" + QStringList::fromVector( originalStrings ).join( "\r\n" );

Message::info( nullptr, Spell::tr( "Strings Removed: %1. New string table has %2 entries." )
.arg( originalStrings.size() ).arg( newSize ), msg
);
Message::info( nullptr, Spell::tr( "Strings Removed: %1. New string table has %2 entries." )
.arg( originalStrings.size() ).arg( newSize ), msg );
}

return QModelIndex();
}

static QModelIndex cast_Static( NifModel * nif, const QModelIndex & index );
};

QModelIndex spRemoveUnusedStrings::cast_Static( NifModel * nif, const QModelIndex & index )
{
spRemoveUnusedStrings tmp;
if ( tmp.isApplicable( nif, index ) )
return tmp.cast( nif, index );
return index;
}

REGISTER_SPELL( spRemoveUnusedStrings )
Loading

0 comments on commit 247fa41

Please sign in to comment.