From 878f9f843362018cdc4f4c6653cd2d4ff4905a57 Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 14 Sep 2025 11:32:35 +0200 Subject: [PATCH] Add extension path type --- .../misc/testresourcehelpers.cpp | 54 +++---- apps/components_tests/vfs/testpathutil.cpp | 134 +++++++++++++----- apps/openmw/mwrender/animation.cpp | 18 +-- apps/openmw/mwrender/objectpaging.cpp | 6 +- apps/openmw/mwsound/soundbuffer.cpp | 11 +- apps/openmw/mwworld/cellpreloader.cpp | 6 +- components/misc/resourcehelpers.cpp | 17 ++- components/misc/resourcehelpers.hpp | 2 +- components/resource/keyframemanager.cpp | 6 +- components/resource/scenemanager.cpp | 12 +- components/vfs/pathutil.hpp | 84 ++++++++++- 11 files changed, 255 insertions(+), 95 deletions(-) diff --git a/apps/components_tests/misc/testresourcehelpers.cpp b/apps/components_tests/misc/testresourcehelpers.cpp index 53d65c4300..7940c1d591 100644 --- a/apps/components_tests/misc/testresourcehelpers.cpp +++ b/apps/components_tests/misc/testresourcehelpers.cpp @@ -12,6 +12,8 @@ namespace Misc::ResourceHelpers constexpr VFS::Path::NormalizedView sound("sound"); constexpr VFS::Path::NormalizedView textures("textures"); constexpr VFS::Path::NormalizedView bookart("bookart"); + constexpr VFS::Path::ExtensionView mp3("mp3"); + constexpr VFS::Path::ExtensionView b("b"); TEST(MiscResourceHelpersCorrectSoundPath, shouldKeepWavExtensionIfExistsInVfs) { @@ -29,74 +31,74 @@ namespace Misc::ResourceHelpers TEST(MiscResourceHelpersCorrectSoundPath, shouldKeepWavExtensionIfBothExistsInVfs) { - constexpr VFS::Path::NormalizedView wav("sound/foo.wav"); - constexpr VFS::Path::NormalizedView mp3("sound/foo.mp3"); + constexpr VFS::Path::NormalizedView wavPath("sound/foo.wav"); + constexpr VFS::Path::NormalizedView mp3Path("sound/foo.mp3"); const std::unique_ptr vfs = TestingOpenMW::createTestVFS({ - { wav, nullptr }, - { mp3, nullptr }, + { wavPath, nullptr }, + { mp3Path, nullptr }, }); - EXPECT_EQ(correctSoundPath(wav, *vfs), "sound/foo.wav"); + EXPECT_EQ(correctSoundPath(wavPath, *vfs), "sound/foo.wav"); } - TEST(MiscResourceHelpersCorrectResourcePath, shouldFallbackToGivenExtentionIfDoesNotExistInVfs) + TEST(MiscResourceHelpersCorrectResourcePath, shouldFallbackToGivenExtensionIfDoesNotExistInVfs) { constexpr VFS::Path::NormalizedView path("sound/foo.wav"); const std::unique_ptr vfs = TestingOpenMW::createTestVFS({}); - EXPECT_EQ(correctResourcePath({ { sound } }, path, *vfs, "mp3"), "sound/foo.mp3"); + EXPECT_EQ(correctResourcePath({ { sound } }, path, *vfs, mp3), "sound/foo.mp3"); } - TEST(MiscResourceHelpersCorrectResourcePath, shouldFallbackToGivenExtentionIfBothExistInVfs) + TEST(MiscResourceHelpersCorrectResourcePath, shouldFallbackToGivenExtensionIfBothExistInVfs) { - constexpr VFS::Path::NormalizedView wav("sound/foo.wav"); - constexpr VFS::Path::NormalizedView mp3("sound/foo.mp3"); + constexpr VFS::Path::NormalizedView wavPath("sound/foo.wav"); + constexpr VFS::Path::NormalizedView mp3Path("sound/foo.mp3"); const std::unique_ptr vfs = TestingOpenMW::createTestVFS({ - { wav, nullptr }, - { mp3, nullptr }, + { wavPath, nullptr }, + { mp3Path, nullptr }, }); - EXPECT_EQ(correctResourcePath({ { sound } }, wav, *vfs, "mp3"), "sound/foo.mp3"); + EXPECT_EQ(correctResourcePath({ { sound } }, wavPath, *vfs, mp3), "sound/foo.mp3"); } - TEST(MiscResourceHelpersCorrectResourcePath, shouldKeepExtentionIfExistInVfs) + TEST(MiscResourceHelpersCorrectResourcePath, shouldKeepExtensionIfExistInVfs) { - constexpr VFS::Path::NormalizedView wav("sound/foo.wav"); + constexpr VFS::Path::NormalizedView wavPath("sound/foo.wav"); const std::unique_ptr vfs = TestingOpenMW::createTestVFS({ - { wav, nullptr }, + { wavPath, nullptr }, }); - EXPECT_EQ(correctResourcePath({ { sound } }, wav, *vfs, "mp3"), "sound/foo.wav"); + EXPECT_EQ(correctResourcePath({ { sound } }, wavPath, *vfs, mp3), "sound/foo.wav"); } TEST(MiscResourceHelpersCorrectResourcePath, shouldPrefixWithGivenTopDirectory) { constexpr VFS::Path::NormalizedView path("foo.mp3"); const std::unique_ptr vfs = TestingOpenMW::createTestVFS({}); - EXPECT_EQ(correctResourcePath({ { sound } }, path, *vfs, "mp3"), "sound/foo.mp3"); + EXPECT_EQ(correctResourcePath({ { sound } }, path, *vfs, mp3), "sound/foo.mp3"); } TEST(MiscResourceHelpersCorrectResourcePath, shouldChangeTopDirectoryAndKeepExtensionIfOriginalExistInVfs) { constexpr VFS::Path::NormalizedView path("bookart/foo.a"); - constexpr VFS::Path::NormalizedView a("textures/foo.a"); + constexpr VFS::Path::NormalizedView aPath("textures/foo.a"); const std::unique_ptr vfs = TestingOpenMW::createTestVFS({ - { a, nullptr }, + { aPath, nullptr }, }); - EXPECT_EQ(correctResourcePath({ { textures, bookart } }, path, *vfs, "b"), "textures/foo.a"); + EXPECT_EQ(correctResourcePath({ { textures, bookart } }, path, *vfs, b), "textures/foo.a"); } TEST(MiscResourceHelpersCorrectResourcePath, shouldChangeTopDirectoryAndChangeExtensionIfFallbackExistInVfs) { constexpr VFS::Path::NormalizedView path("bookart/foo.a"); - constexpr VFS::Path::NormalizedView b("textures/foo.b"); + constexpr VFS::Path::NormalizedView bPath("textures/foo.b"); const std::unique_ptr vfs = TestingOpenMW::createTestVFS({ - { b, nullptr }, + { bPath, nullptr }, }); - EXPECT_EQ(correctResourcePath({ { textures, bookart } }, path, *vfs, "b"), "textures/foo.b"); + EXPECT_EQ(correctResourcePath({ { textures, bookart } }, path, *vfs, b), "textures/foo.b"); } TEST(MiscResourceHelpersCorrectResourcePath, shouldHandlePathEqualToDirectory) { constexpr VFS::Path::NormalizedView path("sound"); const std::unique_ptr vfs = TestingOpenMW::createTestVFS({}); - EXPECT_EQ(correctResourcePath({ { sound } }, path, *vfs, "mp3"), "sound/sound"); + EXPECT_EQ(correctResourcePath({ { sound } }, path, *vfs, mp3), "sound/sound"); } struct MiscResourceHelpersCorrectResourcePathShouldRemoveExtraPrefix : TestWithParam @@ -106,7 +108,7 @@ namespace Misc::ResourceHelpers TEST_P(MiscResourceHelpersCorrectResourcePathShouldRemoveExtraPrefix, shouldMatchExpected) { const std::unique_ptr vfs = TestingOpenMW::createTestVFS({}); - EXPECT_EQ(correctResourcePath({ { sound } }, GetParam(), *vfs, "mp3"), "sound/foo.mp3"); + EXPECT_EQ(correctResourcePath({ { sound } }, GetParam(), *vfs, mp3), "sound/foo.mp3"); } const std::vector pathsWithPrefix = { diff --git a/apps/components_tests/vfs/testpathutil.cpp b/apps/components_tests/vfs/testpathutil.cpp index 36b2ba2347..41f9a22f78 100644 --- a/apps/components_tests/vfs/testpathutil.cpp +++ b/apps/components_tests/vfs/testpathutil.cpp @@ -10,6 +10,13 @@ namespace VFS::Path { using namespace testing; + template + struct TypePair + { + using Type0 = T0; + using Type1 = T1; + }; + struct VFSPathIsNormalizedTest : TestWithParam> { }; @@ -53,6 +60,80 @@ namespace VFS::Path EXPECT_EQ(value, "foo"); } + TEST(VFSPathExtensionViewTest, shouldSupportDefaultConstructor) + { + constexpr ExtensionView extension; + EXPECT_TRUE(extension.empty()); + EXPECT_EQ(extension.value(), ""); + } + + TEST(VFSPathExtensionViewTest, shouldSupportConstexprConstructorFromConstCharPtr) + { + constexpr ExtensionView extension("png"); + EXPECT_FALSE(extension.empty()); + EXPECT_EQ(extension.value(), "png"); + } + + TEST(VFSPathExtensionViewTest, constructorShouldThrowExceptionOnNotNormalizedValue) + { + EXPECT_THROW([] { ExtensionView("PNG"); }(), std::invalid_argument); + } + + TEST(VFSPathExtensionViewTest, constructorShouldThrowExceptionIfValueContainsExtensionSeparator) + { + EXPECT_THROW([] { ExtensionView(".png"); }(), std::invalid_argument); + } + + TEST(VFSPathExtensionViewTest, constructorShouldThrowExceptionIfValueContainsSeparator) + { + EXPECT_THROW([] { ExtensionView("/png"); }(), std::invalid_argument); + } + + template + struct VFSPathExtensionViewOperatorsTest : Test + { + }; + + TYPED_TEST_SUITE_P(VFSPathExtensionViewOperatorsTest); + + TYPED_TEST_P(VFSPathExtensionViewOperatorsTest, supportsEqual) + { + using Type0 = typename TypeParam::Type0; + using Type1 = typename TypeParam::Type1; + const Type0 extension{ "png" }; + const Type1 otherEqual{ "png" }; + const Type1 otherNotEqual{ "jpg" }; + EXPECT_EQ(extension, otherEqual); + EXPECT_EQ(otherEqual, extension); + EXPECT_NE(extension, otherNotEqual); + EXPECT_NE(otherNotEqual, extension); + } + + TYPED_TEST_P(VFSPathExtensionViewOperatorsTest, supportsLess) + { + using Type0 = typename TypeParam::Type0; + using Type1 = typename TypeParam::Type1; + const Type0 extension{ "png" }; + const Type1 otherEqual{ "png" }; + const Type1 otherLess{ "jpg" }; + const Type1 otherGreater{ "tga" }; + EXPECT_FALSE(extension < otherEqual); + EXPECT_FALSE(otherEqual < extension); + EXPECT_LT(otherLess, extension); + EXPECT_FALSE(extension < otherLess); + EXPECT_LT(extension, otherGreater); + EXPECT_FALSE(otherGreater < extension); + } + + REGISTER_TYPED_TEST_SUITE_P(VFSPathExtensionViewOperatorsTest, supportsEqual, supportsLess); + + using VFSPathExtensionViewOperatorsTypePairs + = Types, TypePair, + TypePair, TypePair>; + + INSTANTIATE_TYPED_TEST_SUITE_P( + Typed, VFSPathExtensionViewOperatorsTest, VFSPathExtensionViewOperatorsTypePairs); + TEST(VFSPathNormalizedTest, shouldSupportDefaultConstructor) { const Normalized value; @@ -131,41 +212,34 @@ namespace VFS::Path TEST(VFSPathNormalizedTest, changeExtensionShouldReplaceAfterLastDot) { - Normalized value("foo/bar.a"); - ASSERT_TRUE(value.changeExtension("so")); - EXPECT_EQ(value.value(), "foo/bar.so"); - } - - TEST(VFSPathNormalizedTest, changeExtensionShouldThrowExceptionOnNotNormalizedExtension) - { - Normalized value("foo/bar.a"); - EXPECT_THROW(value.changeExtension("\\SO"), std::invalid_argument); + Normalized value("foo/ba.r.a"); + constexpr ExtensionView extension("so"); + ASSERT_TRUE(value.changeExtension(extension)); + EXPECT_EQ(value.value(), "foo/ba.r.so"); } TEST(VFSPathNormalizedTest, changeExtensionShouldIgnorePathWithoutADot) { Normalized value("foo/bar"); - ASSERT_FALSE(value.changeExtension("so")); + constexpr ExtensionView extension("so"); + ASSERT_FALSE(value.changeExtension(extension)); EXPECT_EQ(value.value(), "foo/bar"); } TEST(VFSPathNormalizedTest, changeExtensionShouldIgnorePathWithDotBeforeSeparator) { Normalized value("foo.bar/baz"); - ASSERT_FALSE(value.changeExtension("so")); + constexpr ExtensionView extension("so"); + ASSERT_FALSE(value.changeExtension(extension)); EXPECT_EQ(value.value(), "foo.bar/baz"); } - TEST(VFSPathNormalizedTest, changeExtensionShouldThrowExceptionOnExtensionWithDot) + TEST(VFSPathNormalizedTest, changeExtensionShouldReplaceWithShorterExtension) { - Normalized value("foo.a"); - EXPECT_THROW(value.changeExtension(".so"), std::invalid_argument); - } - - TEST(VFSPathNormalizedTest, changeExtensionShouldThrowExceptionOnExtensionWithSeparator) - { - Normalized value("foo.a"); - EXPECT_THROW(value.changeExtension("so/"), std::invalid_argument); + Normalized value("foo/bar.nif"); + constexpr ExtensionView extension("kf"); + ASSERT_TRUE(value.changeExtension(extension)); + EXPECT_EQ(value.value(), "foo/bar.kf"); } TEST(VFSPathNormalizedTest, filenameShouldReturnLastComponentOfThePath) @@ -218,20 +292,14 @@ namespace VFS::Path REGISTER_TYPED_TEST_SUITE_P(VFSPathNormalizedOperatorsTest, supportsEqual, supportsLess); - template - struct TypePair - { - using Type0 = T0; - using Type1 = T1; - }; + using VFSPathNormalizedOperatorsTypePairs + = Types, TypePair, + TypePair, TypePair, + TypePair, TypePair, + TypePair, TypePair, + TypePair, TypePair>; - using TypePairs = Types, TypePair, - TypePair, TypePair, - TypePair, TypePair, - TypePair, TypePair, - TypePair, TypePair>; - - INSTANTIATE_TYPED_TEST_SUITE_P(Typed, VFSPathNormalizedOperatorsTest, TypePairs); + INSTANTIATE_TYPED_TEST_SUITE_P(Typed, VFSPathNormalizedOperatorsTest, VFSPathNormalizedOperatorsTypePairs); TEST(VFSPathNormalizedViewTest, shouldSupportConstructorFromNormalized) { diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index d13a081045..1b573f9ce0 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -659,21 +659,21 @@ namespace MWRender path.replace(extensionStart, path.size() - extensionStart, "/"); + constexpr VFS::Path::ExtensionView kf("kf"); for (const VFS::Path::Normalized& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator(path)) - { - if (Misc::getFileExtension(name) == "kf") - { + if (name.extension() == kf) addSingleAnimSource(name, baseModel); - } - } } void Animation::addAnimSource(std::string_view model, const std::string& baseModel) { + constexpr VFS::Path::ExtensionView kf("kf"); + constexpr VFS::Path::ExtensionView nif("nif"); + VFS::Path::Normalized kfname(model); - if (Misc::getFileExtension(kfname) == "nif") - kfname.changeExtension("kf"); + if (kfname.extension() == nif) + kfname.changeExtension(kf); addSingleAnimSource(kfname, baseModel); @@ -757,10 +757,12 @@ namespace MWRender // Get the blending rules if (Settings::game().mSmoothAnimTransitions) { + constexpr VFS::Path::ExtensionView yaml("yaml"); + // Note, even if the actual config is .json - we should send a .yaml path to AnimBlendRulesManager, the // manager will check for .json if it will not find a specified .yaml file. VFS::Path::Normalized blendConfigPath(kfname); - blendConfigPath.changeExtension("yaml"); + blendConfigPath.changeExtension(yaml); // globalBlendConfigPath is only used with actors! Objects have no default blending. constexpr VFS::Path::NormalizedView globalBlendConfigPath("animations/animation-config.yaml"); diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp index c7c92f61c9..043624acc9 100644 --- a/apps/openmw/mwrender/objectpaging.cpp +++ b/apps/openmw/mwrender/objectpaging.cpp @@ -744,10 +744,12 @@ namespace MWRender if (activeGrid && type != ESM::REC_STAT && type != ESM::REC_STAT4) { model = Misc::ResourceHelpers::correctActorModelPath(model, mSceneManager->getVFS()); - if (Misc::getFileExtension(model) == "nif") + constexpr VFS::Path::ExtensionView nif("nif"); + if (model.extension() == nif) { VFS::Path::Normalized kfname = model; - kfname.changeExtension("kf"); + constexpr VFS::Path::ExtensionView kf("kf"); + kfname.changeExtension(kf); if (mSceneManager->getVFS()->exists(kfname)) continue; } diff --git a/apps/openmw/mwsound/soundbuffer.cpp b/apps/openmw/mwsound/soundbuffer.cpp index e558764bfa..ee002a1cac 100644 --- a/apps/openmw/mwsound/soundbuffer.cpp +++ b/apps/openmw/mwsound/soundbuffer.cpp @@ -20,6 +20,7 @@ namespace MWSound namespace { constexpr VFS::Path::NormalizedView soundDir("sound"); + constexpr VFS::Path::ExtensionView mp3("mp3"); struct AudioParams { @@ -202,9 +203,8 @@ namespace MWSound SoundBuffer* SoundBufferPool::insertSound(const ESM::RefId& soundId, const ESM4::Sound& sound) { - VFS::Path::Normalized path - = Misc::ResourceHelpers::correctResourcePath({ { soundDir } }, VFS::Path::toNormalized(sound.mSoundFile), - *MWBase::Environment::get().getResourceSystem()->getVFS(), "mp3"); + VFS::Path::Normalized path = Misc::ResourceHelpers::correctResourcePath({ { soundDir } }, + VFS::Path::toNormalized(sound.mSoundFile), *MWBase::Environment::get().getResourceSystem()->getVFS(), mp3); float volume = 1, min = 1, max = 255; // TODO: needs research SoundBuffer& sfx = mSoundBuffers.emplace_back(std::move(path), volume, min, max); mBufferNameMap.emplace(soundId, &sfx); @@ -213,9 +213,8 @@ namespace MWSound SoundBuffer* SoundBufferPool::insertSound(const ESM::RefId& soundId, const ESM4::SoundReference& sound) { - VFS::Path::Normalized path - = Misc::ResourceHelpers::correctResourcePath({ { soundDir } }, VFS::Path::toNormalized(sound.mSoundFile), - *MWBase::Environment::get().getResourceSystem()->getVFS(), "mp3"); + VFS::Path::Normalized path = Misc::ResourceHelpers::correctResourcePath({ { soundDir } }, + VFS::Path::toNormalized(sound.mSoundFile), *MWBase::Environment::get().getResourceSystem()->getVFS(), mp3); float volume = 1, min = 1, max = 255; // TODO: needs research // TODO: sound.mSoundId can link to another SoundReference, probably we will need to add additional lookups to // ESMStore. diff --git a/apps/openmw/mwworld/cellpreloader.cpp b/apps/openmw/mwworld/cellpreloader.cpp index 8aa95878f6..9824fa3a1d 100644 --- a/apps/openmw/mwworld/cellpreloader.cpp +++ b/apps/openmw/mwworld/cellpreloader.cpp @@ -122,10 +122,12 @@ namespace MWWorld if (!vfs.exists(mesh)) continue; - if (Misc::getFileName(mesh).starts_with('x') && Misc::getFileExtension(mesh) == "nif") + constexpr VFS::Path::ExtensionView nif("nif"); + if (Misc::getFileName(mesh).starts_with('x') && mesh.extension() == nif) { kfname = mesh; - kfname.changeExtension("kf"); + constexpr VFS::Path::ExtensionView kf("kf"); + kfname.changeExtension(kf); if (vfs.exists(kfname)) mPreloadedObjects.insert(mKeyframeManager->get(kfname)); } diff --git a/components/misc/resourcehelpers.cpp b/components/misc/resourcehelpers.cpp index 2808d7dfec..69a899810c 100644 --- a/components/misc/resourcehelpers.cpp +++ b/components/misc/resourcehelpers.cpp @@ -19,7 +19,10 @@ namespace constexpr VFS::Path::NormalizedView bookart("bookart"); constexpr VFS::Path::NormalizedView icons("icons"); constexpr VFS::Path::NormalizedView materials("materials"); - constexpr std::string_view dds("dds"); + constexpr VFS::Path::ExtensionView dds("dds"); + constexpr VFS::Path::ExtensionView kf("kf"); + constexpr VFS::Path::ExtensionView nif("nif"); + constexpr VFS::Path::ExtensionView mp3("mp3"); bool changeExtension(std::string& path, std::string_view ext) { @@ -67,7 +70,7 @@ bool Misc::ResourceHelpers::changeExtensionToDds(std::string& path) // If `ext` is not empty we first search file with extension `ext`, then if not found fallback to original extension. VFS::Path::Normalized Misc::ResourceHelpers::correctResourcePath( std::span topLevelDirectories, VFS::Path::NormalizedView resPath, - const VFS::Manager& vfs, std::string_view ext) + const VFS::Manager& vfs, VFS::Path::ExtensionView ext) { VFS::Path::Normalized correctedPath; @@ -167,8 +170,8 @@ VFS::Path::Normalized Misc::ResourceHelpers::correctActorModelPath( mdlname.insert(mdlname.begin(), 'x'); VFS::Path::Normalized kfname(mdlname); - if (Misc::getFileExtension(mdlname) == "nif") - kfname.changeExtension("kf"); + if (kfname.extension() == nif) + kfname.changeExtension(kf); if (!vfs->exists(kfname)) return VFS::Path::Normalized(resPath); @@ -215,16 +218,16 @@ VFS::Path::Normalized Misc::ResourceHelpers::correctSoundPath( VFS::Path::NormalizedView resPath, const VFS::Manager& vfs) { // Note: likely should be replaced with - // return correctResourcePath({ { "sound" } }, resPath, vfs, "mp3"); + // return correctResourcePath({ { "sound" } }, resPath, vfs, mp3); // but there is a slight difference in behaviour: - // - `correctResourcePath(..., "mp3")` first checks `.mp3`, then tries the original extension + // - `correctResourcePath(..., mp3)` first checks `.mp3`, then tries the original extension // - the implementation below first tries the original extension, then falls back to `.mp3`. // Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav. if (!vfs.exists(resPath)) { VFS::Path::Normalized sound(resPath); - sound.changeExtension("mp3"); + sound.changeExtension(mp3); return sound; } return VFS::Path::Normalized(resPath); diff --git a/components/misc/resourcehelpers.hpp b/components/misc/resourcehelpers.hpp index cbf5c8fea4..d0b429cb99 100644 --- a/components/misc/resourcehelpers.hpp +++ b/components/misc/resourcehelpers.hpp @@ -26,7 +26,7 @@ namespace Misc { bool changeExtensionToDds(std::string& path); VFS::Path::Normalized correctResourcePath(std::span topLevelDirectories, - VFS::Path::NormalizedView resPath, const VFS::Manager& vfs, std::string_view ext = {}); + VFS::Path::NormalizedView resPath, const VFS::Manager& vfs, VFS::Path::ExtensionView ext = {}); VFS::Path::Normalized correctTexturePath(VFS::Path::NormalizedView resPath, const VFS::Manager& vfs); VFS::Path::Normalized correctIconPath(VFS::Path::NormalizedView resPath, const VFS::Manager& vfs); VFS::Path::Normalized correctBookartPath(VFS::Path::NormalizedView resPath, const VFS::Manager& vfs); diff --git a/components/resource/keyframemanager.cpp b/components/resource/keyframemanager.cpp index e216271925..fdc4e05774 100644 --- a/components/resource/keyframemanager.cpp +++ b/components/resource/keyframemanager.cpp @@ -52,7 +52,8 @@ namespace Resource , mPath(path) , mVFS(&vfs) { - mPath.changeExtension("txt"); + constexpr VFS::Path::ExtensionView txt("txt"); + mPath.changeExtension(txt); } bool RetrieveAnimationsVisitor::belongsToLeftUpperExtremity(const std::string& name) @@ -214,7 +215,8 @@ namespace Resource return osg::ref_ptr(static_cast(obj.get())); osg::ref_ptr loaded(new SceneUtil::KeyframeHolder); - if (Misc::getFileExtension(name.value()) == "kf") + constexpr VFS::Path::ExtensionView kf("kf"); + if (name.extension() == kf) { auto file = std::make_shared(name); Nif::Reader reader(*file, mEncoder); diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index 687d0730b3..2acf3fb5e8 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -947,10 +947,20 @@ namespace Resource osg::ref_ptr SceneManager::loadErrorMarker() { + constexpr VFS::Path::ExtensionView meshTypes[] = { + VFS::Path::ExtensionView("nif"), + VFS::Path::ExtensionView("osg"), + VFS::Path::ExtensionView("osgt"), + VFS::Path::ExtensionView("osgb"), + VFS::Path::ExtensionView("osgx"), + VFS::Path::ExtensionView("osg2"), + VFS::Path::ExtensionView("dae"), + }; + try { VFS::Path::Normalized path("meshes/marker_error.****"); - for (const auto meshType : { "nif", "osg", "osgt", "osgb", "osgx", "osg2", "dae" }) + for (const VFS::Path::ExtensionView meshType : meshTypes) { path.changeExtension(meshType); if (mVFS->exists(path)) diff --git a/components/vfs/pathutil.hpp b/components/vfs/pathutil.hpp index a8915ab5da..ab6281a92b 100644 --- a/components/vfs/pathutil.hpp +++ b/components/vfs/pathutil.hpp @@ -107,6 +107,67 @@ namespace VFS::Path return std::find_if(begin, end, [](char v) { return v == extensionSeparator || v == separator; }); } + inline constexpr bool isExtension(std::string_view value) + { + return isNormalized(value) && findSeparatorOrExtensionSeparator(value.begin(), value.end()) == value.end(); + } + + class NormalizedView; + + class ExtensionView + { + public: + constexpr ExtensionView() noexcept = default; + + constexpr explicit ExtensionView(const char* value) + : mValue(value) + { + if (!isExtension(mValue)) + throw std::invalid_argument( + "ExtensionView value is invalid extension: \"" + std::string(mValue) + "\""); + } + + constexpr std::string_view value() const noexcept { return mValue; } + + constexpr bool empty() const noexcept { return mValue.empty(); } + + friend constexpr bool operator==(const ExtensionView& lhs, const ExtensionView& rhs) = default; + + friend constexpr bool operator==(const ExtensionView& lhs, const auto& rhs) { return lhs.mValue == rhs; } + +#if defined(_MSC_VER) && _MSC_VER <= 1935 + friend constexpr bool operator==(const auto& lhs, const ExtensionView& rhs) + { + return lhs == rhs.mValue; + } +#endif + + friend constexpr bool operator<(const ExtensionView& lhs, const ExtensionView& rhs) + { + return lhs.mValue < rhs.mValue; + } + + friend constexpr bool operator<(const ExtensionView& lhs, const auto& rhs) + { + return lhs.mValue < rhs; + } + + friend constexpr bool operator<(const auto& lhs, const ExtensionView& rhs) + { + return lhs < rhs.mValue; + } + + friend std::ostream& operator<<(std::ostream& stream, const ExtensionView& value) + { + return stream << value.mValue; + } + + private: + std::string_view mValue; + + friend class NormalizedView; + }; + class Normalized; class NormalizedView @@ -191,6 +252,15 @@ namespace VFS::Path return result; } + constexpr ExtensionView extension() const + { + ExtensionView result; + if (const std::size_t position = mValue.find_last_of(extensionSeparator); + position != std::string_view::npos) + result.mValue = mValue.substr(position + 1); + return result; + } + private: std::string_view mValue; }; @@ -238,18 +308,13 @@ namespace VFS::Path operator const std::string&() const { return mValue; } - bool changeExtension(std::string_view extension) + bool changeExtension(ExtensionView extension) { - if (!isNormalized(extension)) - throw std::invalid_argument("Not normalized extension: " + std::string(extension)); - if (findSeparatorOrExtensionSeparator(extension.begin(), extension.end()) != extension.end()) - throw std::invalid_argument("Invalid extension: " + std::string(extension)); const auto it = findSeparatorOrExtensionSeparator(mValue.rbegin(), mValue.rend()); if (it == mValue.rend() || *it == separator) return false; const std::string::difference_type pos = mValue.rend() - it; - mValue.replace(pos, mValue.size(), extension); - std::transform(mValue.begin() + pos, mValue.end(), mValue.begin() + pos, normalize); + mValue.replace(pos, mValue.size(), extension.value()); return true; } @@ -342,6 +407,11 @@ namespace VFS::Path return NormalizedView(*this).filename(); } + ExtensionView extension() const + { + return NormalizedView(*this).extension(); + } + private: std::string mValue; };