diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f533a1a..ad6dc9a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ == CHANGELOG == +* Added new spell (Batch/Copy and Rename all Meshes) by 1OfAKindMods to copy and rename all .mesh files used by a Starfield model. Note that the spell expects a loose NIF file, if the model is from an archive, the renamed mesh files are written under the NifSkope installation directory. +* The right click menu on header strings that end with .bgsm, .bgem or .mat has a new option to browse materials. Updating the view (Alt+U) may be needed for material path changes to take effect. +* Added limited support for rendering translucent Starfield materials. +* Fixed the contrast parameter of Starfield blenders that is inverted in the material data. +* The default height scale for Starfield parallax occlusion mapping is now 0.0 in the render settings, as height textures do not actually work in the game. + #### NifSkope-2.0.dev9-20240616 * Starfield material rendering improvements, including support for layered materials with PositionContrast blend mode. CharacterCombine mode is also rendered, but it is interpreted as linear blending. diff --git a/build/nif.xml b/build/nif.xml index 0dcd2017..34d4aabe 100644 --- a/build/nif.xml +++ b/build/nif.xml @@ -20,7 +20,7 @@ Skyrim and later. SSE and later. Fallout 76 and later. - Starfield and later. + Starfield and later. SSE only. Fallout 4 strictly, excluding stream 132 and 139 in dev files. Fallout 4/76 including dev files. @@ -29,7 +29,7 @@ Bethesda 132 and later. Bethesda 152 and later. Fallout 76 stream 155 only. - SSE, FO4, FO76 + SSE, FO4, FO76 FO4, FO76 Bethesda 20.2 only. Divinity 2 @@ -215,7 +215,7 @@ {{Fallout 4}} Fallout 4 (LS_Mirelurk.nif, Screen.nif) {{Fallout 76}} - {{Starfield}} + {{Starfield}} {{Empire Earth III}}, {{FFT Online}}, Atlantica Online, IRIS Online, Wizard101 QQSpeed Emerge @@ -1967,8 +1967,8 @@ - - + + diff --git a/lib/libfo76utils b/lib/libfo76utils index a03424b8..f24877b0 160000 --- a/lib/libfo76utils +++ b/lib/libfo76utils @@ -1 +1 @@ -Subproject commit a03424b8f76ed47763c1cd2db72d876aeba05663 +Subproject commit f24877b0bee9acdf164ecd91e3e6dd023df8abec diff --git a/res/shaders/stf_default.frag b/res/shaders/stf_default.frag index 6cd43b59..bd6da3ef 100644 --- a/res/shaders/stf_default.frag +++ b/res/shaders/stf_default.frag @@ -359,6 +359,9 @@ float getBlenderMask(int n) vec2 parallaxMapping( int n, vec3 V, vec2 offset ) { + if ( parallaxOcclusionSettings.z < 0.0005 ) + return offset; // disabled + // determine optimal height of each layer float layerHeight = 1.0 / mix( parallaxOcclusionSettings.y, parallaxOcclusionSettings.x, abs(V.z) ); @@ -446,6 +449,7 @@ void main() vec3 pbrMap = vec3(0.75, 0.0, 1.0); // roughness, metalness, AO float alpha = 1.0; vec3 emissive = vec3(0.0); + vec3 transmissive = vec3(0.0); for (int i = 0; i < 4; i++) { if ( !lm.layersEnabled[i] ) @@ -498,7 +502,7 @@ void main() if ( lm.blenders[i - 1].blendMode == 2 ) { float blendPosition = lm.blenders[i - 1].floatParams[2]; float blendContrast = lm.blenders[i - 1].floatParams[3]; - blendContrast = max( (1.0 - blendContrast) * min(blendPosition, 1.0 - blendPosition), 0.001 ); + blendContrast = max( blendContrast * min(blendPosition, 1.0 - blendPosition), 0.001 ); blendPosition = ( blendPosition - 0.5 ) * 3.17; blendPosition = ( blendPosition * blendPosition + 1.0 ) * blendPosition + 0.5; float maskMin = blendPosition - blendContrast; @@ -581,6 +585,12 @@ void main() continue; emissive += getLayerTexture( i, 7, offset ).rgb * tmp.rgb * tmp.a; } + + if ( lm.layers[i].material.textureSet.textures[8] != 0 ) { + // _transmissive.dds + if ( lm.translucencySettings.isEnabled && i == lm.translucencySettings.transmittanceSourceLayer ) + transmissive = vec3( getLayerTexture( i, 8, offset ).r * lm.translucencySettings.transmissiveScale ); + } } normal = normalize( btnMatrix_norm * normal ); @@ -667,8 +677,6 @@ void main() float ao = pbrMap.b; refl *= f * envLUT.g * ao; - // TODO: translucency - // Diffuse color.rgb = diffuse * albedo * D.rgb; // Ambient @@ -680,6 +688,17 @@ void main() // Emissive color.rgb += emissive; + // Transmissive + if ( lm.translucencySettings.isEnabled && lm.translucencySettings.isThin ) { + transmissive *= albedo * ( vec3(1.0) - f ); + // TODO: implement flipBackFaceNormalsInViewSpace + color.rgb += transmissive * D.rgb * max( -NdotL, 0.0 ); + if ( hasCubeMap ) + color.rgb += textureLod( CubeMap2, -normalWS, 0.0 ).rgb * transmissive * A.rgb * ao; + else + color.rgb += transmissive * A.rgb * ( ao * 0.08 ); + } + color.rgb = tonemap(color.rgb * D.a, A.a); color.a = baseMap.a * alpha; diff --git a/res/shaders/stf_default.prog b/res/shaders/stf_default.prog index 7b613e42..2f1b0263 100644 --- a/res/shaders/stf_default.prog +++ b/res/shaders/stf_default.prog @@ -2,7 +2,7 @@ checkgroup begin and # Starfield - check HEADER/BS Header/BS Version >= 172 + check HEADER/BS Header/BS Version >= 170 check BSGeometry check BSLightingShaderProperty checkgroup end diff --git a/res/shaders/stf_effectshader.prog b/res/shaders/stf_effectshader.prog index a1f5ec62..89c7a68a 100644 --- a/res/shaders/stf_effectshader.prog +++ b/res/shaders/stf_effectshader.prog @@ -2,7 +2,7 @@ checkgroup begin and # Starfield - check HEADER/BS Header/BS Version == 172 + check HEADER/BS Header/BS Version >= 170 check BSGeometry check BSEffectShaderProperty checkgroup end diff --git a/src/gamemanager.cpp b/src/gamemanager.cpp index 983febc7..e5560b47 100644 --- a/src/gamemanager.cpp +++ b/src/gamemanager.cpp @@ -430,6 +430,7 @@ GameMode GameManager::get_game( const NifModel * nif ) return FALLOUT_4; case BSSTREAM_155: return FALLOUT_76; + case BSSTREAM_170: case BSSTREAM_172: case BSSTREAM_173: return STARFIELD; diff --git a/src/gamemanager.h b/src/gamemanager.h index bf896623..2f509107 100644 --- a/src/gamemanager.h +++ b/src/gamemanager.h @@ -59,6 +59,7 @@ enum BSVersion BSSTREAM_100 = 100, BSSTREAM_130 = 130, BSSTREAM_155 = 155, + BSSTREAM_170 = 170, BSSTREAM_172 = 172, BSSTREAM_173 = 173 }; diff --git a/src/gl/glproperty.cpp b/src/gl/glproperty.cpp index 4b7d0f97..bb73ef76 100644 --- a/src/gl/glproperty.cpp +++ b/src/gl/glproperty.cpp @@ -851,7 +851,7 @@ void BSShaderLightingProperty::updateImpl( const NifModel * nif, const QModelInd if ( index == iBlock ) { bsVersion = (unsigned short) nif->getBSVersion(); - if ( bsVersion >= 160 ) { + if ( bsVersion >= 170 ) { setSFMaterial( name ); } else { if ( bsVersion < 83 ) @@ -1072,7 +1072,7 @@ enum QString BSShaderLightingProperty::fileName( int id ) const { // Starfield (not implemented here) - if ( bsVersion >= 160 ) + if ( bsVersion >= 170 ) return QString(); // Fallout 4 or 76 BGSM file @@ -1241,7 +1241,7 @@ void BSLightingShaderProperty::updateImpl( const NifModel * nif, const QModelInd BSShaderLightingProperty::updateImpl( nif, index ); if ( index == iBlock ) { - if ( name.endsWith(".bgsm", Qt::CaseInsensitive) && bsVersion < 160 ) { + if ( name.endsWith(".bgsm", Qt::CaseInsensitive) && bsVersion < 170 ) { setMaterial( new ShaderMaterial( name, nif ) ); if ( bsVersion >= 151 ) const_cast< NifModel * >(nif)->loadFO76Material( index, material ); @@ -1307,7 +1307,7 @@ void BSLightingShaderProperty::updateParams( const NifModel * nif ) { resetParams(); - if ( bsVersion >= 172 ) { + if ( bsVersion >= 170 ) { setSFMaterial( nif->get( iBlock, "Name" ) ); return; } @@ -1482,7 +1482,7 @@ void BSEffectShaderProperty::updateImpl( const NifModel * nif, const QModelIndex BSShaderLightingProperty::updateImpl( nif, index ); if ( index == iBlock ) { - if ( name.endsWith(".bgem", Qt::CaseInsensitive) && bsVersion < 160 ) { + if ( name.endsWith(".bgem", Qt::CaseInsensitive) && bsVersion < 170 ) { setMaterial( new EffectMaterial( name, nif ) ); if ( bsVersion >= 151 ) const_cast< NifModel * >(nif)->loadFO76Material( index, material ); diff --git a/src/gl/glshape.cpp b/src/gl/glshape.cpp index d070cbe4..59665d50 100644 --- a/src/gl/glshape.cpp +++ b/src/gl/glshape.cpp @@ -205,7 +205,7 @@ void Shape::updateShader() else if ( alphaProperty && alphaProperty->hasAlphaBlend() ) drawInSecondPass = true; else if ( bssp ) { - if ( bssp->bsVersion >= 160 ) { + if ( bssp->bsVersion >= 170 ) { const CE2Material * sfMat = nullptr; bssp->getSFMaterial( sfMat, scene->nifModel ); if ( sfMat && ( sfMat->shaderRoute != 0 || (sfMat->flags & CE2Material::Flag_IsDecal) ) ) diff --git a/src/gl/renderer.cpp b/src/gl/renderer.cpp index 47975212..eb553e2b 100644 --- a/src/gl/renderer.cpp +++ b/src/gl/renderer.cpp @@ -442,7 +442,7 @@ void Renderer::updateSettings() cfg.useShaders = settings.value( "Settings/Render/General/Use Shaders", true ).toBool(); cfg.sfParallaxMaxSteps = short( settings.value( "Settings/Render/General/Sf Parallax Steps", 200 ).toInt() ); - cfg.sfParallaxScale = settings.value( "Settings/Render/General/Sf Parallax Scale", 0.033f).toFloat(); + cfg.sfParallaxScale = settings.value( "Settings/Render/General/Sf Parallax Scale", 0.0f).toFloat(); cfg.sfParallaxOffset = settings.value( "Settings/Render/General/Sf Parallax Offset", 0.5f).toFloat(); cfg.cubeMapPathFO76 = settings.value( "Settings/Render/General/Cube Map Path FO 76", "textures/shared/cubemaps/mipblur_defaultoutside1.dds" ).toString(); cfg.cubeMapPathSTF = settings.value( "Settings/Render/General/Cube Map Path STF", "textures/cubemaps/cell_cityplazacube.dds" ).toString(); @@ -957,6 +957,24 @@ bool Renderer::setupProgramSF( Program * prog, Shape * mesh ) prog->uni1b_l( prog->uniLocation("lm.emissiveSettings.isEnabled"), false ); } + // translucency settings + if ( mat->flags & CE2Material::Flag_Translucency ) { + const CE2Material::TranslucencySettings * sp = mat->translucencySettings; + prog->uni1b_l( prog->uniLocation("lm.translucencySettings.isEnabled"), sp->isEnabled ); + prog->uni1b_l( prog->uniLocation("lm.translucencySettings.isThin"), sp->isThin ); + prog->uni1b_l( prog->uniLocation("lm.translucencySettings.flipBackFaceNormalsInViewSpace"), sp->flipBackFaceNormalsInVS ); + prog->uni1b_l( prog->uniLocation("lm.translucencySettings.useSSS"), sp->useSSS ); + prog->uni1f_l( prog->uniLocation("lm.translucencySettings.sssWidth"), sp->sssWidth ); + prog->uni1f_l( prog->uniLocation("lm.translucencySettings.sssStrength"), sp->sssStrength ); + prog->uni1f_l( prog->uniLocation("lm.translucencySettings.transmissiveScale"), sp->transmissiveScale ); + prog->uni1f_l( prog->uniLocation("lm.translucencySettings.transmittanceWidth"), sp->transmittanceWidth ); + prog->uni1f_l( prog->uniLocation("lm.translucencySettings.specLobe0RoughnessScale"), sp->specLobe0RoughnessScale ); + prog->uni1f_l( prog->uniLocation("lm.translucencySettings.specLobe1RoughnessScale"), sp->specLobe1RoughnessScale ); + prog->uni1i_l( prog->uniLocation("lm.translucencySettings.transmittanceSourceLayer"), sp->sourceLayer ); + } else { + prog->uni1b_l( prog->uniLocation("lm.translucencySettings.isEnabled"), false ); + } + // decal settings if ( mat->flags & CE2Material::Flag_IsDecal ) { const CE2Material::DecalSettings * sp = mat->decalSettings; diff --git a/src/gl/renderer.h b/src/gl/renderer.h index 381848b9..9429e392 100644 --- a/src/gl/renderer.h +++ b/src/gl/renderer.h @@ -411,7 +411,7 @@ public slots: { bool useShaders = true; short sfParallaxMaxSteps = 200; - float sfParallaxScale = 0.033f; + float sfParallaxScale = 0.0f; float sfParallaxOffset = 0.5f; QString cubeMapPathFO76; QString cubeMapPathSTF; diff --git a/src/io/MeshFile.cpp b/src/io/MeshFile.cpp index 5235f347..9b89c041 100644 --- a/src/io/MeshFile.cpp +++ b/src/io/MeshFile.cpp @@ -42,7 +42,7 @@ quint32 MeshFile::readMesh() quint32 magic; in >> magic; - if ( magic != 1 && magic != 2 ) + if ( magic > 2U ) return 0; quint32 indicesSize; @@ -173,18 +173,20 @@ quint32 MeshFile::readMesh() weights[i] = BoneWeightsUNorm(weightsUNORM, i); } - quint32 numLODs; - in >> numLODs; - lods.resize(numLODs); - for ( quint32 i = 0; i < numLODs; i++ ) { - quint32 indicesSize2; - in >> indicesSize2; - lods[i].resize(indicesSize2 / 3); - - for ( quint32 j = 0; j < indicesSize2 / 3; j++ ) { - Triangle tri; - in >> tri; - lods[i][j] = tri; + if ( magic ) { + quint32 numLODs; + in >> numLODs; + lods.resize(numLODs); + for ( quint32 i = 0; i < numLODs; i++ ) { + quint32 indicesSize2; + in >> indicesSize2; + lods[i].resize(indicesSize2 / 3); + + for ( quint32 j = 0; j < indicesSize2 / 3; j++ ) { + Triangle tri; + in >> tri; + lods[i][j] = tri; + } } } diff --git a/src/lib/importex/importex.cpp b/src/lib/importex/importex.cpp index 5c85e078..817cf4a6 100644 --- a/src/lib/importex/importex.cpp +++ b/src/lib/importex/importex.cpp @@ -69,9 +69,9 @@ struct ImportExportOption QVector impexOptions{ - ImportExportOption{ ".OBJ", importObj, exportObj, 0, 171 }, - ImportExportOption{ ".OBJ as Collision", importObjAsCollision, nullptr, 0, 171 }, - ImportExportOption{ ".glTF", nullptr, exportGltf, 172 }, + ImportExportOption{ ".OBJ", importObj, exportObj, 0, 169 }, + ImportExportOption{ ".OBJ as Collision", importObjAsCollision, nullptr, 0, 169 }, + ImportExportOption{ ".glTF", nullptr, exportGltf, 170 }, }; @@ -126,4 +126,4 @@ void NifSkope::sltExport( QAction* a ) if ( impex.exportFn ) { impex.exportFn(nif, ogl->scene, index); } -} \ No newline at end of file +} diff --git a/src/model/nifextfiles.cpp b/src/model/nifextfiles.cpp index af7c7c0f..137fff80 100644 --- a/src/model/nifextfiles.cpp +++ b/src/model/nifextfiles.cpp @@ -85,7 +85,7 @@ void NifModel::loadSFBlender( NifItem * parent, const void * o ) setValue( parent, "Height Blend Threshold", floatParams[0] ); setValue( parent, "Height Blend Factor", floatParams[1] ); setValue( parent, "Position", floatParams[2] ); - setValue( parent, "Contrast", floatParams[3] ); + setValue( parent, "Contrast", 1.0f - floatParams[3] ); setValue( parent, "Mask Intensity", floatParams[4] ); setValue( parent, "Blend Color", boolParams[0] ); setValue( parent, "Blend Metalness", boolParams[1] ); diff --git a/src/spells/filerename.cpp b/src/spells/filerename.cpp index 72e862e6..45da4998 100644 --- a/src/spells/filerename.cpp +++ b/src/spells/filerename.cpp @@ -20,7 +20,7 @@ class spResourceRename final : public Spell { public: QString name() const override final { return Spell::tr( "Search/Replace Resource Paths" ); } - QString page() const override final { return Spell::tr( "" ); } + QString page() const override final { return Spell::tr( "Batch" ); } QIcon icon() const override final { return QIcon(); diff --git a/src/spells/headerstring.cpp b/src/spells/headerstring.cpp index 557102cb..0fc2ad0e 100644 --- a/src/spells/headerstring.cpp +++ b/src/spells/headerstring.cpp @@ -96,6 +96,7 @@ class spEditStringIndex final : public Spell return false; } + static QString browseMaterial( const NifModel * nif, const QString & matPath ); void browseMaterial( QLineEdit * le, const NifModel * nif ); QModelIndex cast( NifModel * nif, const QModelIndex & index ) override final @@ -170,7 +171,7 @@ static bool bgsmFileNameFilterFunc( [[maybe_unused]] void * p, const std::string return ( s.starts_with( "materials/" ) && ( s.ends_with( ".bgsm" ) || s.ends_with( ".bgem" ) ) ); } -void spEditStringIndex::browseMaterial( QLineEdit * le, const NifModel * nif ) +QString spEditStringIndex::browseMaterial( const NifModel * nif, const QString & matPath ) { std::set< std::string_view > materials; AllocBuffers stringBuf; @@ -184,16 +185,69 @@ void spEditStringIndex::browseMaterial( QLineEdit * le, const NifModel * nif ) } std::string prvPath; - if ( !le->text().isEmpty() ) - prvPath = Game::GameManager::get_full_path( le->text(), "materials", ( bsVersion >= 170 ? ".mat" : nullptr ) ); + if ( !matPath.isEmpty() ) + prvPath = Game::GameManager::get_full_path( matPath, "materials", ( bsVersion >= 170 ? ".mat" : nullptr ) ); FileBrowserWidget fileBrowser( 800, 600, "Select Material", materials, prvPath ); if ( fileBrowser.exec() == QDialog::Accepted ) { const std::string_view * s = fileBrowser.getItemSelected(); if ( s ) - le->setText( QString::fromUtf8( s->data(), qsizetype(s->length()) ) ); + return QString::fromUtf8( s->data(), qsizetype(s->length()) ); } + return QString(); +} + +void spEditStringIndex::browseMaterial( QLineEdit * le, const NifModel * nif ) +{ + QString newPath( browseMaterial( nif, le->text() ) ); + if ( !newPath.isEmpty() ) + le->setText( newPath ); } REGISTER_SPELL( spEditStringIndex ) +//! Browse a material path stored as header string +class spBrowseHeaderMaterialPath final : public Spell +{ +public: + QString name() const override final { return Spell::tr( "Browse Material" ); } + QString page() const override final { return Spell::tr( "" ); } + QIcon icon() const override final + { + if ( !txt_xpm_icon ) + txt_xpm_icon = QIconPtr( new QIcon(QPixmap( txt_xpm )) ); + + return *txt_xpm_icon; + } + bool constant() const override final { return true; } + bool instant() const override final { return true; } + + bool isApplicable( const NifModel * nif, const QModelIndex & index ) override final + { + if ( !( nif && nif->getBSVersion() >= 130 ) ) + return false; + auto block = nif->getTopItem( index ); + if ( !( block && block == nif->getHeaderItem() ) ) + return false; + const NifItem * item = nif->getItem( index ); + if ( !( item && item->valueType() == NifValue::tSizedString ) ) + return false; + QString s( item->getValueAsString() ); + return ( s.endsWith( ".bgsm", Qt::CaseInsensitive ) || s.endsWith( ".bgem", Qt::CaseInsensitive ) || s.endsWith( ".mat", Qt::CaseInsensitive ) ); + } + + QModelIndex cast( NifModel * nif, const QModelIndex & index ) override final + { + NifItem * item = nif->getItem( index ); + if ( !( item && item->valueType() == NifValue::tSizedString ) ) + return index; + + QString newPath( spEditStringIndex::browseMaterial( nif, item->getValueAsString() ) ); + if ( !newPath.isEmpty() ) + item->setValueFromString( newPath ); + + return index; + } +}; + +REGISTER_SPELL( spBrowseHeaderMaterialPath ) diff --git a/src/spells/mesh.cpp b/src/spells/mesh.cpp index 69ec0932..e2bf30f5 100644 --- a/src/spells/mesh.cpp +++ b/src/spells/mesh.cpp @@ -825,7 +825,7 @@ class spUpdateBounds final : public Spell bool isApplicable( const NifModel * nif, const QModelIndex & index ) override final { - if ( nif->getBSVersion() >= 172 && nif->blockInherits( index, "BSGeometry" ) ) + if ( nif->getBSVersion() >= 170 && nif->blockInherits( index, "BSGeometry" ) ) return true; return nif->blockInherits( index, "BSTriShape" ) && nif->getIndex( index, "Vertex Data" ).isValid(); } @@ -834,7 +834,7 @@ class spUpdateBounds final : public Spell QModelIndex cast( NifModel * nif, const QModelIndex & index ) override final { - if ( nif->getBSVersion() >= 172 && nif->blockInherits( index, "BSGeometry" ) ) + if ( nif->getBSVersion() >= 170 && nif->blockInherits( index, "BSGeometry" ) ) return cast_Starfield( nif, index ); auto vertData = nif->getIndex( index, "Vertex Data" ); diff --git a/src/spells/meshfilecopy.cpp b/src/spells/meshfilecopy.cpp index 03c7b899..bde84195 100644 --- a/src/spells/meshfilecopy.cpp +++ b/src/spells/meshfilecopy.cpp @@ -24,7 +24,7 @@ class spResourceCopy final : public Spell { public: QString name() const override final { return Spell::tr( "Copy and Rename all Meshes" ); } - QString page() const override final { return Spell::tr( "" ); } + QString page() const override final { return Spell::tr( "Batch" ); } QIcon icon() const override final { return QIcon(); @@ -34,35 +34,35 @@ class spResourceCopy final : public Spell bool isApplicable( const NifModel * nif, const QModelIndex & index ) override final { - return ( nif && !index.isValid() ); + return ( nif && nif->getBSVersion() >= 170 && !index.isValid() ); } - void copyPaths(NifModel* nif, NifItem* item, const QString& author, const QString& project, const QString& nifFolder); - NifItem* findChildByName(NifItem* parent, const QString& name); - QString sanitizeFileName(const QString& input); + void copyPaths(NifModel* nif, NifItem* item, const QString& author, const QString& project, const QString& nifFolder); + NifItem* findChildByName(NifItem* parent, const QString& name); + QString sanitizeFileName(const QString& input); QModelIndex cast(NifModel* nif, const QModelIndex& index) override final; }; QString spResourceCopy::sanitizeFileName(const QString& input) { - // Convert to lowercase - QString sanitized = input.toLower(); - - // Remove periods, spaces, and slashes - sanitized.remove(QRegularExpression("[\\.\\s/\\\\]")); - - // Replace special filesystem characters with an underscore - QMap specialChars = { - {'<', '_'}, {'>', '_'}, {':', '_'}, {'"', '_'}, - {'|', '_'}, {'?', '_'}, {'*', '_'} - }; - for (auto it = specialChars.constBegin(); it != specialChars.constEnd(); ++it) { - sanitized.replace(it.key(), it.value()); - } - - // Convert Unicode characters to ASCII representation - QString result; + // Convert to lowercase + QString sanitized = input.toLower(); + + // Remove periods, spaces, and slashes + sanitized.remove(QRegularExpression("[\\.\\s/\\\\]")); + + // Replace special filesystem characters with an underscore + QMap specialChars = { + {'<', '_'}, {'>', '_'}, {':', '_'}, {'"', '_'}, + {'|', '_'}, {'?', '_'}, {'*', '_'} + }; + for (auto it = specialChars.constBegin(); it != specialChars.constEnd(); ++it) { + sanitized.replace(it.key(), it.value()); + } + + // Convert Unicode characters to ASCII representation + QString result; for (int i = 0; i < sanitized.size(); ++i) { QChar c = sanitized.at(i); if (c.unicode() < 128) { @@ -83,46 +83,46 @@ QString spResourceCopy::sanitizeFileName(const QString& input) } result.append(asciiChunk); finder.setPosition(nextBoundary); - } - if (result.isEmpty() || result.back() != '_') { - result.append('_'); - } - } - } + } + if (result.isEmpty() || result.back() != '_') { + result.append('_'); + } + } + } - // Remove any remaining non-ASCII characters - result.remove(QRegularExpression("[^\\x20-\\x7E]")); + // Remove any remaining non-ASCII characters + result.remove(QRegularExpression("[^\\x20-\\x7E]")); - return result; + return result; } NifItem* spResourceCopy::findChildByName(NifItem* parent, const QString& name) { - for (int i = 0; i < parent->childCount(); i++) { - NifItem* child = parent->child(i); - if (child && child->name() == name) { - return child; - } - } - return nullptr; + for (int i = 0; i < parent->childCount(); i++) { + NifItem* child = parent->child(i); + if (child && child->name() == name) { + return child; + } + } + return nullptr; } void spResourceCopy::copyPaths(NifModel* nif, NifItem* item, const QString& author, const QString& project, const QString& nifFolder) { - if (item && item->name() == "BSGeometry") { - QString objectName = nif->get(item, "Name"); - NifItem* meshArrayItems = findChildByName(item, "Meshes"); + if (item && item->name() == "BSGeometry") { + QString objectName = nif->get(item, "Name"); + NifItem* meshArrayItems = findChildByName(item, "Meshes"); - if (meshArrayItems) { - for (int i = 0; i < meshArrayItems->childCount(); i++) { + if (meshArrayItems) { + for (int i = 0; i < meshArrayItems->childCount(); i++) { NifItem* meshArrayItem = meshArrayItems->child(i); - if (!nif->get(meshArrayItem, "Has Mesh")) { - continue; - } + if (!nif->get(meshArrayItem, "Has Mesh")) { + continue; + } NifItem* mesh = findChildByName(meshArrayItem, "Mesh"); - if (mesh) { + if (mesh) { // The nif field doesn't include the .mesh extension, and always uses a forward slash - QString meshPath = nif->get(mesh, "Mesh Path"); + QString meshPath = nif->get(mesh, "Mesh Path"); // Not using QDir because file as stored uses forward slashes and no extension QString newMeshPath = author + "/" + project + "/" + sanitizeFileName(objectName) + "_lod" + QString::number(i + 1); @@ -135,60 +135,67 @@ void spResourceCopy::copyPaths(NifModel* nif, NifItem* item, const QString& auth newDir.mkpath(newDir.absolutePath()); // Copy the file (platform-independent with the slashes) - QFile::copy(QDir::fromNativeSeparators(oldPath), QDir::fromNativeSeparators(newPath)); + if ( !QFile::copy(QDir::fromNativeSeparators(oldPath), QDir::fromNativeSeparators(newPath)) ) { + QByteArray meshData; + if ( nif->getResourceFile(meshData, meshPath, "geometries/", ".mesh") ) { + QFile newFile( QDir::fromNativeSeparators(newPath) ); + if ( newFile.open(QIODevice::WriteOnly) ) + (void) newFile.write( meshData ); + } + } // Update the value in the nif - findChildByName(mesh,"Mesh Path")->setValueFromString(newMeshPath); - } - } - } - // BSGeometri are leaf structures so no need to process children + findChildByName(mesh,"Mesh Path")->setValueFromString(newMeshPath); + } + } + } + // BSGeometri are leaf structures so no need to process children } else { - // Process children - for (int i = 0; i < item->childCount(); i++) { - if (item->child(i)) { + // Process children + for (int i = 0; i < item->childCount(); i++) { + if (item->child(i)) { copyPaths(nif, item->child(i), author, project, nifFolder); - } - } - } + } + } + } } QModelIndex spResourceCopy::cast(NifModel* nif, const QModelIndex& index) { - if (!nif) - return index; + if (!nif) + return index; - QDialog dlg; - QLabel* lb = new QLabel(&dlg); - lb->setAlignment(Qt::AlignCenter); + QDialog dlg; + QLabel* lb = new QLabel(&dlg); + lb->setAlignment(Qt::AlignCenter); lb->setText(tr("Copy and rename meshes to this format:\ngeometries/author/project/objectname_lod#")); - QLabel* lb1 = new QLabel(&dlg); - lb1->setText(tr("Author Prefix:")); - QLineEdit* le1 = new QLineEdit(&dlg); - le1->setFocus(); + QLabel* lb1 = new QLabel(&dlg); + lb1->setText(tr("Author Prefix:")); + QLineEdit* le1 = new QLineEdit(&dlg); + le1->setFocus(); - QLabel* lb2 = new QLabel(&dlg); - lb2->setText(tr("Project Name:")); - QLineEdit* le2 = new QLineEdit(&dlg); - le2->setFocus(); + QLabel* lb2 = new QLabel(&dlg); + lb2->setText(tr("Project Name:")); + QLineEdit* le2 = new QLineEdit(&dlg); + le2->setFocus(); - QPushButton* bo = new QPushButton(tr("Ok"), &dlg); - QObject::connect(bo, &QPushButton::clicked, &dlg, &QDialog::accept); + QPushButton* bo = new QPushButton(tr("Ok"), &dlg); + QObject::connect(bo, &QPushButton::clicked, &dlg, &QDialog::accept); - QPushButton* bc = new QPushButton(tr("Cancel"), &dlg); - QObject::connect(bc, &QPushButton::clicked, &dlg, &QDialog::reject); + QPushButton* bc = new QPushButton(tr("Cancel"), &dlg); + QObject::connect(bc, &QPushButton::clicked, &dlg, &QDialog::reject); - QGridLayout* grid = new QGridLayout; - dlg.setLayout(grid); - grid->addWidget(lb, 0, 0, 1, 2); - grid->addWidget(lb1, 1, 0, 1, 2); - grid->addWidget(le1, 2, 0, 1, 2); - grid->addWidget(lb2, 3, 0, 1, 2); - grid->addWidget(le2, 4, 0, 1, 2); - grid->addWidget(bo, 5, 0, 1, 1); - grid->addWidget(bc, 5, 1, 1, 1); + QGridLayout* grid = new QGridLayout; + dlg.setLayout(grid); + grid->addWidget(lb, 0, 0, 1, 2); + grid->addWidget(lb1, 1, 0, 1, 2); + grid->addWidget(le1, 2, 0, 1, 2); + grid->addWidget(lb2, 3, 0, 1, 2); + grid->addWidget(le2, 4, 0, 1, 2); + grid->addWidget(bo, 5, 0, 1, 1); + grid->addWidget(bc, 5, 1, 1, 1); if (dlg.exec() != QDialog::Accepted) { return index; @@ -198,13 +205,13 @@ QModelIndex spResourceCopy::cast(NifModel* nif, const QModelIndex& index) QString authorPrefix = sanitizeFileName(le1->text().trimmed().remove(QRegularExpression("^[\\\\/]+|[\\\\/]+$"))); QString projectName = sanitizeFileName(le2->text().trimmed().remove(QRegularExpression("^[\\\\/]+|[\\\\/]+$"))); - for (int b = 0; b < nif->getBlockCount(); b++) { - NifItem* item = nif->getBlockItem(quint32(b)); - if (item) + for (int b = 0; b < nif->getBlockCount(); b++) { + NifItem* item = nif->getBlockItem(quint32(b)); + if (item) copyPaths(nif, item, authorPrefix, projectName, nif->getFolder()); - } + } - return index; + return index; } -REGISTER_SPELL( spResourceCopy ) \ No newline at end of file +REGISTER_SPELL( spResourceCopy ) diff --git a/src/spells/sanitize.cpp b/src/spells/sanitize.cpp index 773d33c3..3a4f9a3d 100644 --- a/src/spells/sanitize.cpp +++ b/src/spells/sanitize.cpp @@ -448,7 +448,7 @@ class spFixInvalidNames final : public Spell } // Fix "Root Material" field - if ( nif->getBSVersion() < 172 && isProp && nif->getIndex(iBlock, "Root Material").isValid() ) { + if ( nif->getBSVersion() < 170 && isProp && nif->getIndex(iBlock, "Root Material").isValid() ) { auto rootIdx = nif->get(iBlock, "Root Material"); auto rootString = nif->get(iBlock, "Root Material"); @@ -665,7 +665,7 @@ QModelIndex spErrorInvalidPaths::cast( NifModel * nif, const QModelIndex & ) auto iBSLSP = nif->getBlockIndex( i, "BSLightingShaderProperty" ); if ( iBSLSP.isValid() ) { checkPath( nif, iBSLSP, "Name", P_NO_EXT ); - if ( nif->getBSVersion() < 172 ) + if ( nif->getBSVersion() < 170 ) checkPath( nif, iBSLSP, "Root Material", P_NO_EXT ); } diff --git a/src/ui/settingsrender.ui b/src/ui/settingsrender.ui index c1a4c1a1..cbe71808 100644 --- a/src/ui/settingsrender.ui +++ b/src/ui/settingsrender.ui @@ -275,7 +275,7 @@ 0.001 - 0.033 + 0.0