diff --git a/.github/workflows/openmw.yml b/.github/workflows/openmw.yml index 9114ae7711..b0e05ea894 100644 --- a/.github/workflows/openmw.yml +++ b/.github/workflows/openmw.yml @@ -83,9 +83,7 @@ jobs: max-size: 1000M - name: Configure - run: | - rm -fr build # remove the build directory - CI/before_script.osx.sh + run: CI/before_script.osx.sh - name: Build run: | cd build diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e106590838..67a227bca7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -501,7 +501,6 @@ Ubuntu_GCC_integration_tests_asan: paths: - ccache/ script: - - rm -fr build # remove the build directory - CI/before_install.osx.sh - export CCACHE_BASEDIR="$(pwd)" - export CCACHE_DIR="$(pwd)/ccache" @@ -521,7 +520,6 @@ Ubuntu_GCC_integration_tests_asan: artifacts: paths: - build/OpenMW-*.dmg - - "build/**/*.log" macOS14_Xcode15_arm64: extends: .MacOS diff --git a/CHANGELOG.md b/CHANGELOG.md index aef21779d0..bcae64b27a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -191,6 +191,7 @@ Bug #8097: GetEffect doesn't detect 0 magnitude spells Bug #8124: Normal weapon resistance is applied twice for NPCs Bug #8132: Actors without hello responses turn to face the player + Bug #8171: Items with more than 100% health can be repaired Feature #1415: Infinite fall failsafe Feature #2566: Handle NAM9 records for manual cell references Feature #3501: OpenMW-CS: Instance Editing - Shortcuts for axial locking diff --git a/CI/before_install.osx.sh b/CI/before_install.osx.sh index 660ecf4adc..0120c55202 100755 --- a/CI/before_install.osx.sh +++ b/CI/before_install.osx.sh @@ -4,14 +4,9 @@ export HOMEBREW_NO_EMOJI=1 export HOMEBREW_NO_INSTALL_CLEANUP=1 export HOMEBREW_AUTOREMOVE=1 -# workaround for gitlab's pre-installed brew -# purge large and unnecessary packages that get in our way and have caused issues -brew uninstall ruby php openjdk node postgresql maven curl || true - brew tap --repair brew update --quiet -# Some of these tools can come from places other than brew, so check before installing brew install curl xquartz gd fontconfig freetype harfbuzz brotli command -v ccache >/dev/null 2>&1 || brew install ccache @@ -27,8 +22,9 @@ cmake --version qmake --version if [[ "${MACOS_AMD64}" ]]; then - curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20221113.zip -o ~/openmw-deps.zip + curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20240802.zip -o ~/openmw-deps.zip + unzip -o ~/openmw-deps.zip -d /tmp > /dev/null else - curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20240802_arm64.zip -o ~/openmw-deps.zip + curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20240818-arm64.tar.xz -o ~/openmw-deps.tar.xz + tar xf ~/openmw-deps.tar.xz -C /tmp > /dev/null fi -unzip -o ~/openmw-deps.zip -d /tmp > /dev/null diff --git a/CI/before_script.osx.sh b/CI/before_script.osx.sh index d3e3698ab2..9f7a5bde8f 100755 --- a/CI/before_script.osx.sh +++ b/CI/before_script.osx.sh @@ -3,6 +3,7 @@ # Silence a git warning git config --global advice.detachedHead false +rm -fr build mkdir build cd build diff --git a/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp b/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp index 14c2a3bfe4..ea9efc3df2 100644 --- a/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp +++ b/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp @@ -33,7 +33,8 @@ namespace { const ObjectId id(&shape); osg::ref_ptr bulletShape(new Resource::BulletShape); - bulletShape->mFileName = "test.nif"; + constexpr VFS::Path::NormalizedView test("test.nif"); + bulletShape->mFileName = test; bulletShape->mFileHash = "test_hash"; ObjectTransform objectTransform; std::fill(std::begin(objectTransform.mPosition.pos), std::end(objectTransform.mPosition.pos), 0.1f); diff --git a/apps/navmeshtool/worldspacedata.cpp b/apps/navmeshtool/worldspacedata.cpp index 3a4ff63329..077b43f72f 100644 --- a/apps/navmeshtool/worldspacedata.cpp +++ b/apps/navmeshtool/worldspacedata.cpp @@ -131,7 +131,8 @@ namespace NavMeshTool osg::ref_ptr shape = [&] { try { - return bulletShapeManager.getShape(Misc::ResourceHelpers::correctMeshPath(model)); + return bulletShapeManager.getShape( + VFS::Path::toNormalized(Misc::ResourceHelpers::correctMeshPath(model))); } catch (const std::exception& e) { diff --git a/apps/opencs/model/prefs/state.cpp b/apps/opencs/model/prefs/state.cpp index 7b3b827583..a3e5a36f65 100644 --- a/apps/opencs/model/prefs/state.cpp +++ b/apps/opencs/model/prefs/state.cpp @@ -408,12 +408,12 @@ void CSMPrefs::State::declare() declareShortcut(mValues->mKeyBindings.mScriptEditorUncomment, "Uncomment Selection"); declareCategory("Models"); - declareString(mValues->mModels.mBaseanim, "base animations").setTooltip("3rd person base model with textkeys-data"); - declareString(mValues->mModels.mBaseanimkna, "base animations, kna") - .setTooltip("3rd person beast race base model with textkeys-data"); - declareString(mValues->mModels.mBaseanimfemale, "base animations, female") - .setTooltip("3rd person female base model with textkeys-data"); - declareString(mValues->mModels.mWolfskin, "base animations, wolf").setTooltip("3rd person werewolf skin"); + declareString(mValues->mModels.mBaseanim, "Base Animations").setTooltip("Third person base model and animations"); + declareString(mValues->mModels.mBaseanimkna, "Base Animations, Beast") + .setTooltip("Third person beast race base model and animations"); + declareString(mValues->mModels.mBaseanimfemale, "Base Animations, Female") + .setTooltip("Third person female base model and animations"); + declareString(mValues->mModels.mWolfskin, "Base Animations, Werewolf").setTooltip("Third person werewolf skin"); } void CSMPrefs::State::declareCategory(const std::string& key) diff --git a/apps/opencs/model/prefs/stringsetting.cpp b/apps/opencs/model/prefs/stringsetting.cpp index 10bd8cb558..4fa2955840 100644 --- a/apps/opencs/model/prefs/stringsetting.cpp +++ b/apps/opencs/model/prefs/stringsetting.cpp @@ -1,6 +1,7 @@ #include "stringsetting.hpp" +#include #include #include @@ -26,17 +27,21 @@ CSMPrefs::StringSetting& CSMPrefs::StringSetting::setTooltip(const std::string& CSMPrefs::SettingWidgets CSMPrefs::StringSetting::makeWidgets(QWidget* parent) { + QLabel* label = new QLabel(getLabel(), parent); + mWidget = new QLineEdit(QString::fromStdString(getValue()), parent); + mWidget->setMinimumWidth(300); if (!mTooltip.empty()) { QString tooltip = QString::fromUtf8(mTooltip.c_str()); + label->setToolTip(tooltip); mWidget->setToolTip(tooltip); } connect(mWidget, &QLineEdit::textChanged, this, &StringSetting::textChanged); - return SettingWidgets{ .mLabel = nullptr, .mInput = mWidget }; + return SettingWidgets{ .mLabel = label, .mInput = mWidget }; } void CSMPrefs::StringSetting::updateWidget() diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 6ab5ab64fa..904b96c463 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -4,13 +4,13 @@ #include "rotationflags.hpp" #include -#include #include #include #include #include #include +#include #include "../mwworld/doorstate.hpp" #include "../mwworld/globalvariablename.hpp" @@ -515,7 +515,7 @@ namespace MWBase /// Spawn a blood effect for \a ptr at \a worldPosition virtual void spawnBloodEffect(const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) = 0; - virtual void spawnEffect(const std::string& model, const std::string& textureOverride, + virtual void spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride, const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true) = 0; diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index 56f69eb906..2e052de768 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -794,18 +794,32 @@ namespace MWGui if (!Settings::gui().mColorTopicEnable) return; - const MyGUI::Colour& specialColour = Settings::gui().mColorTopicSpecific; - const MyGUI::Colour& oldColour = Settings::gui().mColorTopicExhausted; - for (const std::string& keyword : mKeywords) { int flag = MWBase::Environment::get().getDialogueManager()->getTopicFlag(ESM::RefId::stringRefId(keyword)); MyGUI::Button* button = mTopicsList->getItemWidget(keyword); + const auto oldCaption = button->getCaption(); + const MyGUI::IntSize oldSize = button->getSize(); + bool changed = false; if (flag & MWBase::DialogueManager::TopicType::Specific) - button->getSubWidgetText()->setTextColour(specialColour); + { + button->changeWidgetSkin("MW_ListLine_Specific"); + changed = true; + } else if (flag & MWBase::DialogueManager::TopicType::Exhausted) - button->getSubWidgetText()->setTextColour(oldColour); + { + button->changeWidgetSkin("MW_ListLine_Exhausted"); + changed = true; + } + + if (changed) + { + button->setCaption(oldCaption); + button->getSubWidgetText()->setWordWrap(true); + button->getSubWidgetText()->setTextAlign(MyGUI::Align::Left); + button->setSize(oldSize); + } } } diff --git a/apps/openmw/mwgui/merchantrepair.cpp b/apps/openmw/mwgui/merchantrepair.cpp index 3be0bb1c06..a59f225e9e 100644 --- a/apps/openmw/mwgui/merchantrepair.cpp +++ b/apps/openmw/mwgui/merchantrepair.cpp @@ -55,7 +55,7 @@ namespace MWGui { int maxDurability = iter->getClass().getItemMaxHealth(*iter); int durability = iter->getClass().getItemHealth(*iter); - if (maxDurability == durability || maxDurability == 0) + if (maxDurability <= durability || maxDurability == 0) continue; int basePrice = iter->getClass().getValue(*iter); diff --git a/apps/openmw/mwgui/sortfilteritemmodel.cpp b/apps/openmw/mwgui/sortfilteritemmodel.cpp index e5b87abff7..fe85ea4bd0 100644 --- a/apps/openmw/mwgui/sortfilteritemmodel.cpp +++ b/apps/openmw/mwgui/sortfilteritemmodel.cpp @@ -288,7 +288,7 @@ namespace MWGui if ((mFilter & Filter_OnlyRepairable) && (!base.getClass().hasItemHealth(base) - || (base.getClass().getItemHealth(base) == base.getClass().getItemMaxHealth(base)) + || (base.getClass().getItemHealth(base) >= base.getClass().getItemMaxHealth(base)) || (base.getType() != ESM::Weapon::sRecordId && base.getType() != ESM::Armor::sRecordId))) return false; diff --git a/apps/openmw/mwlua/animationbindings.cpp b/apps/openmw/mwlua/animationbindings.cpp index 95294f46cb..b74a3e51c3 100644 --- a/apps/openmw/mwlua/animationbindings.cpp +++ b/apps/openmw/mwlua/animationbindings.cpp @@ -319,14 +319,14 @@ namespace MWLua std::string texture = options->get_or("particleTextureOverride", ""); float scale = options->get_or("scale", 1.f); context.mLuaManager->addAction( - [world, model = std::string(model), texture = std::move(texture), worldPos, scale, + [world, model = VFS::Path::Normalized(model), texture = std::move(texture), worldPos, scale, magicVfx]() { world->spawnEffect(model, texture, worldPos, scale, magicVfx); }, "openmw.vfx.spawn"); } else { - context.mLuaManager->addAction( - [world, model = std::string(model), worldPos]() { world->spawnEffect(model, "", worldPos); }, + context.mLuaManager->addAction([world, model = VFS::Path::Normalized(model), + worldPos]() { world->spawnEffect(model, "", worldPos); }, "openmw.vfx.spawn"); } }; diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index cf55ac63e6..9f45fdb744 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -238,6 +238,11 @@ namespace MWLua } void LuaManager::synchronizedUpdate() + { + mLua.protectedCall([&](LuaUtil::LuaView&) { synchronizedUpdateUnsafe(); }); + } + + void LuaManager::synchronizedUpdateUnsafe() { if (mNewGameStarted) { diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index 5fa20d377f..d75b033a43 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -170,6 +170,7 @@ namespace MWLua LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr, std::optional autoStartConf = std::nullopt); void reloadAllScriptsImpl(); + void synchronizedUpdateUnsafe(); bool mInitialized = false; bool mGlobalScriptsStarted = false; diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 3f2a9b46bb..401ba0ae86 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -214,7 +214,7 @@ namespace const ESM::Static* const fx = world->getStore().get().search(ESM::RefId::stringRefId("VFX_Soul_Trap")); if (fx != nullptr) - world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel), "", + world->spawnEffect(VFS::Path::toNormalized(Misc::ResourceHelpers::correctMeshPath(fx->mModel)), "", creature.getRefData().getPosition().asVec3()); MWBase::Environment::get().getSoundManager()->playSound3D( @@ -1806,7 +1806,8 @@ namespace MWMechanics ESM::RefId::stringRefId("VFX_Summon_End")); if (fx) MWBase::Environment::get().getWorld()->spawnEffect( - Misc::ResourceHelpers::correctMeshPath(fx->mModel), "", ptr.getRefData().getPosition().asVec3()); + VFS::Path::toNormalized(Misc::ResourceHelpers::correctMeshPath(fx->mModel)), "", + ptr.getRefData().getPosition().asVec3()); // Remove the summoned creature's summoned creatures as well MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index a3761e1b64..46f6440ae6 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -846,14 +846,12 @@ namespace MWMechanics mAI = true; } - bool MechanicsManager::isBoundItem(const MWWorld::Ptr& item) + namespace { - static std::set boundItemIDCache; - - // If this is empty then we haven't executed the GMST cache logic yet; or there isn't any sMagicBound* GMST's - // for some reason - if (boundItemIDCache.empty()) + std::set makeBoundItemIdCache() { + std::set boundItemIDCache; + // Build a list of known bound item ID's const MWWorld::Store& gameSettings = MWBase::Environment::get().getESMStore()->get(); @@ -870,15 +868,16 @@ namespace MWMechanics boundItemIDCache.insert(ESM::RefId::stringRefId(currentGMSTValue)); } - } - // Perform bound item check and assign the Flag_Bound bit if it passes - const ESM::RefId& tempItemID = item.getCellRef().getRefId(); + return boundItemIDCache; + } + } - if (boundItemIDCache.count(tempItemID) != 0) - return true; + bool MechanicsManager::isBoundItem(const MWWorld::Ptr& item) + { + static const std::set boundItemIdCache = makeBoundItemIdCache(); - return false; + return boundItemIdCache.find(item.getCellRef().getRefId()) != boundItemIdCache.end(); } bool MechanicsManager::isAllowedToUse(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim) diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index d22b6c4837..1d847a4129 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -72,12 +72,13 @@ namespace MWMechanics { if (effectInfo.mData.mRange == ESM::RT_Target) world->spawnEffect( - Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel), texture, mHitPosition, 1.0f); + VFS::Path::toNormalized(Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel)), texture, + mHitPosition, 1.0f); continue; } else - world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel), texture, mHitPosition, - static_cast(effectInfo.mData.mArea * 2)); + world->spawnEffect(VFS::Path::toNormalized(Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel)), + texture, mHitPosition, static_cast(effectInfo.mData.mArea * 2)); // Play explosion sound (make sure to use NoTrack, since we will delete the projectile now) { @@ -539,7 +540,8 @@ namespace MWMechanics } scale = std::max(scale, 1.f); MWBase::Environment::get().getWorld()->spawnEffect( - Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), effect->mParticle, pos, scale); + VFS::Path::toNormalized(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel)), + effect->mParticle, pos, scale); } if (animation && !mCaster.getClass().isActor()) diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 20a9c38b0f..5601b486d0 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -412,9 +412,9 @@ namespace MWPhysics if (ptr.mRef->mData.mPhysicsPostponed) return; - std::string animationMesh = mesh; - if (ptr.getClass().useAnim()) - animationMesh = Misc::ResourceHelpers::correctActorModelPath(mesh, mResourceSystem->getVFS()); + const VFS::Path::Normalized animationMesh = ptr.getClass().useAnim() + ? Misc::ResourceHelpers::correctActorModelPath(mesh, mResourceSystem->getVFS()) + : mesh; osg::ref_ptr shapeInstance = mShapeManager->getInstance(animationMesh); if (!shapeInstance || !shapeInstance->mCollisionShape) return; @@ -562,7 +562,8 @@ namespace MWPhysics void PhysicsSystem::addActor(const MWWorld::Ptr& ptr, const std::string& mesh) { - std::string animationMesh = Misc::ResourceHelpers::correctActorModelPath(mesh, mResourceSystem->getVFS()); + const VFS::Path::Normalized animationMesh + = Misc::ResourceHelpers::correctActorModelPath(mesh, mResourceSystem->getVFS()); osg::ref_ptr shape = mShapeManager->getShape(animationMesh); // Try to get shape from basic model as fallback for creatures @@ -570,7 +571,7 @@ namespace MWPhysics { if (animationMesh != mesh) { - shape = mShapeManager->getShape(mesh); + shape = mShapeManager->getShape(VFS::Path::toNormalized(mesh)); } } @@ -590,7 +591,8 @@ namespace MWPhysics int PhysicsSystem::addProjectile( const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius) { - osg::ref_ptr shapeInstance = mShapeManager->getInstance(mesh); + osg::ref_ptr shapeInstance + = mShapeManager->getInstance(VFS::Path::toNormalized(mesh)); assert(shapeInstance); float radius = computeRadius ? shapeInstance->mCollisionBox.mExtents.length() / 2.f : 1.f; diff --git a/apps/openmw/mwrender/actoranimation.cpp b/apps/openmw/mwrender/actoranimation.cpp index 35ff81a9ca..b4983087b5 100644 --- a/apps/openmw/mwrender/actoranimation.cpp +++ b/apps/openmw/mwrender/actoranimation.cpp @@ -67,31 +67,29 @@ namespace MWRender } PartHolderPtr ActorAnimation::attachMesh( - const std::string& model, std::string_view bonename, bool enchantedGlow, osg::Vec4f* glowColor) + VFS::Path::NormalizedView model, std::string_view bonename, const osg::Vec4f* glowColor) { osg::Group* parent = getBoneByName(bonename); if (!parent) return nullptr; - osg::ref_ptr instance - = mResourceSystem->getSceneManager()->getInstance(VFS::Path::toNormalized(model), parent); + osg::ref_ptr instance = mResourceSystem->getSceneManager()->getInstance(model, parent); const NodeMap& nodeMap = getNodeMap(); NodeMap::const_iterator found = nodeMap.find(bonename); if (found == nodeMap.end()) return {}; - if (enchantedGlow) + if (glowColor != nullptr) mGlowUpdater = SceneUtil::addEnchantedGlow(instance, mResourceSystem, *glowColor); return std::make_unique(instance); } osg::ref_ptr ActorAnimation::attach( - const std::string& model, std::string_view bonename, std::string_view bonefilter, bool isLight) + VFS::Path::NormalizedView model, std::string_view bonename, std::string_view bonefilter, bool isLight) { - osg::ref_ptr templateNode - = mResourceSystem->getSceneManager()->getTemplate(VFS::Path::toNormalized(model)); + osg::ref_ptr templateNode = mResourceSystem->getSceneManager()->getTemplate(model); const NodeMap& nodeMap = getNodeMap(); auto found = nodeMap.find(bonename); @@ -218,20 +216,20 @@ namespace MWRender return; } - std::string mesh = getSheathedShieldMesh(*shield); + const VFS::Path::Normalized mesh = getSheathedShieldMesh(*shield); if (mesh.empty()) return; - std::string_view boneName = "Bip01 AttachShield"; - osg::Vec4f glowColor = shield->getClass().getEnchantmentColor(*shield); - const std::string holsteredName = addSuffixBeforeExtension(mesh, "_sh"); - bool isEnchanted = !shield->getClass().getEnchantment(*shield).empty(); + constexpr std::string_view boneName = "Bip01 AttachShield"; + const bool isEnchanted = !shield->getClass().getEnchantment(*shield).empty(); + const osg::Vec4f glowColor = isEnchanted ? shield->getClass().getEnchantmentColor(*shield) : osg::Vec4f(); + const VFS::Path::Normalized holsteredName = addSuffixBeforeExtension(mesh, "_sh"); // If we have no dedicated sheath model, use basic shield model as fallback. if (!mResourceSystem->getVFS()->exists(holsteredName)) - mHolsteredShield = attachMesh(mesh, boneName, isEnchanted, &glowColor); + mHolsteredShield = attachMesh(mesh, boneName, isEnchanted ? &glowColor : nullptr); else - mHolsteredShield = attachMesh(holsteredName, boneName, isEnchanted, &glowColor); + mHolsteredShield = attachMesh(holsteredName, boneName, isEnchanted ? &glowColor : nullptr); if (!mHolsteredShield) return; @@ -245,8 +243,7 @@ namespace MWRender // file. if (shieldNode && !shieldNode->getNumChildren()) { - osg::ref_ptr fallbackNode - = mResourceSystem->getSceneManager()->getInstance(VFS::Path::toNormalized(mesh), shieldNode); + osg::ref_ptr fallbackNode = mResourceSystem->getSceneManager()->getInstance(mesh, shieldNode); if (isEnchanted) SceneUtil::addEnchantedGlow(shieldNode, mResourceSystem, glowColor); } @@ -341,20 +338,24 @@ namespace MWRender if (MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Thrown) showHolsteredWeapons = false; - std::string mesh = weapon->getClass().getCorrectedModel(*weapon); - std::string_view boneName = getHolsteredWeaponBoneName(*weapon); - if (mesh.empty() || boneName.empty()) + const VFS::Path::Normalized mesh = weapon->getClass().getCorrectedModel(*weapon); + if (mesh.empty()) + return; + + const std::string_view boneName = getHolsteredWeaponBoneName(*weapon); + if (boneName.empty()) return; // If the scabbard is not found, use the weapon mesh as fallback. - const std::string scabbardName = addSuffixBeforeExtension(mesh, "_sh"); - bool isEnchanted = !weapon->getClass().getEnchantment(*weapon).empty(); + const VFS::Path::Normalized scabbardName = addSuffixBeforeExtension(mesh, "_sh"); + const bool isEnchanted = !weapon->getClass().getEnchantment(*weapon).empty(); if (!mResourceSystem->getVFS()->exists(scabbardName)) { if (showHolsteredWeapons) { - osg::Vec4f glowColor = weapon->getClass().getEnchantmentColor(*weapon); - mScabbard = attachMesh(mesh, boneName, isEnchanted, &glowColor); + const osg::Vec4f glowColor + = isEnchanted ? weapon->getClass().getEnchantmentColor(*weapon) : osg::Vec4f(); + mScabbard = attachMesh(mesh, boneName, isEnchanted ? &glowColor : nullptr); if (mScabbard) resetControllers(mScabbard->getNode()); } @@ -384,7 +385,7 @@ namespace MWRender if (!weaponNode->getNumChildren()) { osg::ref_ptr fallbackNode - = mResourceSystem->getSceneManager()->getInstance(VFS::Path::toNormalized(mesh), weaponNode); + = mResourceSystem->getSceneManager()->getInstance(mesh, weaponNode); resetControllers(fallbackNode); } diff --git a/apps/openmw/mwrender/actoranimation.hpp b/apps/openmw/mwrender/actoranimation.hpp index b6586d4eab..0182df9370 100644 --- a/apps/openmw/mwrender/actoranimation.hpp +++ b/apps/openmw/mwrender/actoranimation.hpp @@ -5,6 +5,8 @@ #include +#include + #include "../mwworld/containerstore.hpp" #include "animation.hpp" @@ -45,21 +47,18 @@ namespace MWRender protected: osg::Group* getBoneByName(std::string_view boneName) const; - virtual void updateHolsteredWeapon(bool showHolsteredWeapons); - virtual void updateHolsteredShield(bool showCarriedLeft); - virtual void updateQuiver(); + void updateHolsteredWeapon(bool showHolsteredWeapons); + void updateHolsteredShield(bool showCarriedLeft); + void updateQuiver(); std::string getShieldMesh(const MWWorld::ConstPtr& shield, bool female) const; virtual std::string getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const; - virtual std::string_view getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon); - virtual PartHolderPtr attachMesh( - const std::string& model, std::string_view bonename, bool enchantedGlow, osg::Vec4f* glowColor); - virtual PartHolderPtr attachMesh(const std::string& model, std::string_view bonename) - { - osg::Vec4f stubColor = osg::Vec4f(0, 0, 0, 0); - return attachMesh(model, bonename, false, &stubColor); - } + std::string_view getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon); + + PartHolderPtr attachMesh( + VFS::Path::NormalizedView model, std::string_view bonename, const osg::Vec4f* glowColor = nullptr); + osg::ref_ptr attach( - const std::string& model, std::string_view bonename, std::string_view bonefilter, bool isLight); + VFS::Path::NormalizedView model, std::string_view bonename, std::string_view bonefilter, bool isLight); PartHolderPtr mScabbard; PartHolderPtr mHolsteredShield; diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 5b7d1db78a..dacf7076ec 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -388,17 +388,20 @@ namespace std::string_view mEffectId; }; - osg::ref_ptr getVFXLightModelInstance() + namespace { - static osg::ref_ptr lightModel = nullptr; - - if (!lightModel) + osg::ref_ptr makeVFXLightModelInstance() { - lightModel = new osg::LightModel; + osg::ref_ptr lightModel = new osg::LightModel; lightModel->setAmbientIntensity({ 1, 1, 1, 1 }); + return lightModel; } - return lightModel; + const osg::ref_ptr& getVFXLightModelInstance() + { + static const osg::ref_ptr lightModel = makeVFXLightModelInstance(); + return lightModel; + } } void assignBoneBlendCallbackRecursive(MWRender::BoneAnimBlendController* controller, osg::Node* parent, bool isRoot) @@ -1508,10 +1511,10 @@ namespace MWRender } animationPath.replace(animationPath.size() - 4, 4, "/"); - for (const auto& name : resourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath)) + for (const VFS::Path::Normalized& name : resourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath)) { if (Misc::getFileExtension(name) == "nif") - loadBonesFromFile(node, VFS::Path::toNormalized(name), resourceSystem); + loadBonesFromFile(node, name, resourceSystem); } } diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index a4c0181d35..123eadfdec 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -533,7 +533,7 @@ namespace MWRender : CharacterPreview( parent, resourceSystem, MWMechanics::getPlayer(), 512, 512, osg::Vec3f(0, 125, 8), osg::Vec3f(0, 0, 8)) , mBase(*mCharacter.get()->mBase) - , mRef(&mBase) + , mRef(ESM::makeBlankCellRef(), &mBase) , mPitchRadians(osg::DegreesToRadians(6.f)) { mCharacter = MWWorld::Ptr(&mRef, nullptr); diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index b6cc823d28..f84e58e4bc 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -111,7 +111,7 @@ namespace MWRender MWWorld::ConstPtr item = *it; std::string_view bonename; - std::string itemModel = item.getClass().getCorrectedModel(item); + VFS::Path::Normalized itemModel = item.getClass().getCorrectedModel(item); if (slot == MWWorld::InventoryStore::Slot_CarriedRight) { if (item.getType() == ESM::Weapon::sRecordId) diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index 08dfcb667c..10bc239dd7 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -484,15 +484,24 @@ namespace MWRender bool is1stPerson = mViewMode == VM_FirstPerson; bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0; + std::string_view base; + if (!isWerewolf) + { + if (!is1stPerson) + base = Settings::models().mXbaseanim.get().value(); + else + base = Settings::models().mXbaseanim1st.get().value(); + } + const std::string defaultSkeleton = Misc::ResourceHelpers::correctActorModelPath( getActorSkeleton(is1stPerson, isFemale, isBeast, isWerewolf), mResourceSystem->getVFS()); std::string smodel = defaultSkeleton; - bool isBase = !isWerewolf; + bool isCustomModel = false; if (!is1stPerson && !isWerewolf && !mNpc->mModel.empty()) { std::string model = Misc::ResourceHelpers::correctMeshPath(mNpc->mModel); - isBase = isDefaultActorSkeleton(model); + isCustomModel = !isDefaultActorSkeleton(model); smodel = Misc::ResourceHelpers::correctActorModelPath(model, mResourceSystem->getVFS()); } @@ -500,33 +509,21 @@ namespace MWRender updateParts(); - if (!is1stPerson) - { - const std::string& base = Settings::models().mXbaseanim.get().value(); - if (!isWerewolf) - addAnimSource(base, smodel); + if (!base.empty()) + addAnimSource(base, smodel); - if (!isBase) - { - addAnimSource(defaultSkeleton, smodel); - addAnimSource(smodel, smodel); - } - else if (base != defaultSkeleton) - { - addAnimSource(defaultSkeleton, smodel); - } + if (defaultSkeleton != base) + addAnimSource(defaultSkeleton, smodel); - if (!isWerewolf && isBeast && mNpc->mRace.contains("argonian")) - addAnimSource("meshes\\xargonian_swimkna.nif", smodel); - } - else - { - if (!isWerewolf) - addAnimSource(Settings::models().mXbaseanim1st.get().value(), smodel); + if (isCustomModel) + addAnimSource(smodel, smodel); - if (!isBase) - addAnimSource(smodel, smodel); + const bool customArgonianSwim = !is1stPerson && !isWerewolf && isBeast && mNpc->mRace.contains("argonian"); + if (customArgonianSwim) + addAnimSource(Settings::models().mXargonianswimkna.get().value(), smodel); + if (is1stPerson) + { mObjectRoot->setNodeMask(Mask_FirstPerson); mObjectRoot->addCullCallback(new OverrideFieldOfViewCallback(mFirstPersonFieldOfView)); } @@ -681,11 +678,11 @@ namespace MWRender PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, std::string_view bonename, std::string_view bonefilter, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight) { - osg::ref_ptr attached = attach(model, bonename, bonefilter, isLight); + osg::ref_ptr attached = attach(VFS::Path::toNormalized(model), bonename, bonefilter, isLight); if (enchantedGlow) mGlowUpdater = SceneUtil::addEnchantedGlow(attached, mResourceSystem, *glowColor); - return std::make_unique(attached); + return std::make_unique(std::move(attached)); } osg::Vec3f NpcAnimation::runAnimation(float timepassed) diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp index 6e39d99404..8040790bd7 100644 --- a/apps/openmw/mwrender/objectpaging.cpp +++ b/apps/openmw/mwrender/objectpaging.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -639,7 +640,7 @@ namespace MWRender continue; const int type = store.findStatic(ref.mRefId); - std::string model = getModel(type, ref.mRefId, store); + VFS::Path::Normalized model = getModel(type, ref.mRefId, store); if (model.empty()) continue; model = Misc::ResourceHelpers::correctMeshPath(model); @@ -647,10 +648,10 @@ namespace MWRender if (activeGrid && type != ESM::REC_STAT) { model = Misc::ResourceHelpers::correctActorModelPath(model, mSceneManager->getVFS()); - std::string kfname = Misc::StringUtils::lowerCase(model); - if (kfname.size() > 4 && kfname.ends_with(".nif")) + if (Misc::getFileExtension(model) == "nif") { - kfname.replace(kfname.size() - 4, 4, ".kf"); + VFS::Path::Normalized kfname = model; + kfname.changeExtension("kf"); if (mSceneManager->getVFS()->exists(kfname)) continue; } @@ -671,7 +672,7 @@ namespace MWRender ->second; } - osg::ref_ptr cnode = mSceneManager->getTemplate(VFS::Path::toNormalized(model), false); + osg::ref_ptr cnode = mSceneManager->getTemplate(model, false); if (activeGrid) { diff --git a/apps/openmw/mwrender/ripples.cpp b/apps/openmw/mwrender/ripples.cpp index d9c1b41737..bb8248217a 100644 --- a/apps/openmw/mwrender/ripples.cpp +++ b/apps/openmw/mwrender/ripples.cpp @@ -206,8 +206,14 @@ namespace MWRender }; // PASS: Blot in all ripple spawners - mProgramBlobber->apply(state); - state.apply(frameState.mStateset); + state.pushStateSet(frameState.mStateset); + state.apply(); + state.applyAttribute(mProgramBlobber); + for (const auto& [name, stack] : state.getUniformMap()) + { + if (!stack.uniformVec.empty()) + state.getLastAppliedProgramObject()->apply(*(stack.uniformVec.back().first)); + } if (mUseCompute) { @@ -225,8 +231,12 @@ namespace MWRender } // PASS: Wave simulation - mProgramSimulation->apply(state); - state.apply(frameState.mStateset); + state.applyAttribute(mProgramSimulation); + for (const auto& [name, stack] : state.getUniformMap()) + { + if (!stack.uniformVec.empty()) + state.getLastAppliedProgramObject()->apply(*(stack.uniformVec.back().first)); + } if (mUseCompute) { @@ -242,6 +252,8 @@ namespace MWRender state.applyTextureAttribute(0, mTextures[1]); osg::Geometry::drawImplementation(renderInfo); } + + state.popStateSet(); } osg::Texture* RipplesSurface::getColorTexture() const diff --git a/apps/openmw/mwworld/cellpreloader.cpp b/apps/openmw/mwworld/cellpreloader.cpp index 05d6efc494..e54e9d84df 100644 --- a/apps/openmw/mwworld/cellpreloader.cpp +++ b/apps/openmw/mwworld/cellpreloader.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -105,8 +106,8 @@ namespace MWWorld } } - std::string mesh; - std::string kfname; + VFS::Path::Normalized mesh; + VFS::Path::Normalized kfname; for (std::string_view path : mMeshes) { if (mAbort) @@ -121,19 +122,15 @@ namespace MWWorld if (!vfs.exists(mesh)) continue; - size_t slashpos = mesh.find_last_of("/\\"); - if (slashpos != std::string::npos && slashpos != mesh.size() - 1) + if (Misc::getFileName(mesh).starts_with('x') && Misc::getFileExtension(mesh) == "nif") { - if (Misc::StringUtils::toLower(mesh[slashpos + 1]) == 'x' - && Misc::StringUtils::ciEndsWith(mesh, ".nif")) - { - kfname = mesh; - kfname.replace(kfname.size() - 4, 4, ".kf"); - if (vfs.exists(kfname)) - mPreloadedObjects.insert(mKeyframeManager->get(kfname)); - } + kfname = mesh; + kfname.changeExtension("kf"); + if (vfs.exists(kfname)) + mPreloadedObjects.insert(mKeyframeManager->get(kfname)); } - mPreloadedObjects.insert(mSceneManager->getTemplate(VFS::Path::toNormalized(mesh))); + + mPreloadedObjects.insert(mSceneManager->getTemplate(mesh)); if (mPreloadInstances) mPreloadedObjects.insert(mBulletShapeManager->cacheInstance(mesh)); else diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index 6f3d23593b..4cd189bb20 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -317,9 +317,9 @@ namespace } // new reference - MWWorld::LiveCellRef ref(record); + MWWorld::LiveCellRef ref(ESM::makeBlankCellRef(), record); ref.load(state); - collection.mList.push_back(ref); + collection.mList.push_back(std::move(ref)); MWWorld::LiveCellRefBase* base = &collection.mList.back(); MWBase::Environment::get().getWorldModel()->registerPtr(MWWorld::Ptr(base, cellstore)); @@ -426,9 +426,9 @@ namespace MWWorld liveCellRef.mData.setDeletedByContentFile(true); if (iter != mList.end()) - *iter = liveCellRef; + *iter = std::move(liveCellRef); else - mList.push_back(liveCellRef); + mList.push_back(std::move(liveCellRef)); } else { @@ -455,7 +455,7 @@ namespace MWWorld LiveCellRef liveCellRef(ref, ptr); if (!isEnabled(ref, esmStore)) liveCellRef.mData.disable(); - list.push_back(liveCellRef); + list.push_back(std::move(liveCellRef)); } template diff --git a/apps/openmw/mwworld/containerstore.cpp b/apps/openmw/mwworld/containerstore.cpp index f48f73f48a..5a7c5b5333 100644 --- a/apps/openmw/mwworld/containerstore.cpp +++ b/apps/openmw/mwworld/containerstore.cpp @@ -101,9 +101,9 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::getState( if (!record) return ContainerStoreIterator(this); - LiveCellRef ref(record); + LiveCellRef ref(ESM::makeBlankCellRef(), record); ref.load(state); - collection.mList.push_back(ref); + collection.mList.push_back(std::move(ref)); auto it = ContainerStoreIterator(this, --collection.mList.end()); MWBase::Environment::get().getWorldModel()->registerPtr(*it); diff --git a/apps/openmw/mwworld/livecellref.cpp b/apps/openmw/mwworld/livecellref.cpp index 61b838bbf0..aaa74f5ef3 100644 --- a/apps/openmw/mwworld/livecellref.cpp +++ b/apps/openmw/mwworld/livecellref.cpp @@ -15,104 +15,122 @@ #include "ptr.hpp" #include "worldmodel.hpp" -MWWorld::LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM::CellRef& cref) - : mClass(&Class::get(type)) - , mRef(cref) - , mData(cref) +namespace MWWorld { -} + LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM::CellRef& cref) + : mClass(&Class::get(type)) + , mRef(cref) + , mData(cref) + { + } -MWWorld::LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM4::Reference& cref) - : mClass(&Class::get(type)) - , mRef(cref) - , mData(cref) -{ -} + LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM4::Reference& cref) + : mClass(&Class::get(type)) + , mRef(cref) + , mData(cref) + { + } -MWWorld::LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM4::ActorCharacter& cref) - : mClass(&Class::get(type)) - , mRef(cref) - , mData(cref) -{ -} + LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM4::ActorCharacter& cref) + : mClass(&Class::get(type)) + , mRef(cref) + , mData(cref) + { + } -MWWorld::LiveCellRefBase::~LiveCellRefBase() -{ - MWBase::Environment::get().getWorldModel()->deregisterLiveCellRef(*this); -} + LiveCellRefBase::LiveCellRefBase(LiveCellRefBase&& other) noexcept + : mClass(other.mClass) + , mRef(std::move(other.mRef)) + , mData(std::move(other.mData)) + , mWorldModel(std::exchange(other.mWorldModel, nullptr)) + { + } -void MWWorld::LiveCellRefBase::loadImp(const ESM::ObjectState& state) -{ - mRef = MWWorld::CellRef(state.mRef); - mData = RefData(state, mData.isDeletedByContentFile()); + LiveCellRefBase::~LiveCellRefBase() + { + if (mWorldModel != nullptr) + mWorldModel->deregisterLiveCellRef(*this); + } - Ptr ptr(this); + LiveCellRefBase& LiveCellRefBase::operator=(LiveCellRefBase&& other) noexcept + { + mClass = other.mClass; + mRef = std::move(other.mRef); + mData = std::move(other.mData); + mWorldModel = std::exchange(other.mWorldModel, nullptr); + return *this; + } - if (state.mHasLocals) + void LiveCellRefBase::loadImp(const ESM::ObjectState& state) { - const ESM::RefId& scriptId = mClass->getScript(ptr); - // Make sure we still have a script. It could have been coming from a content file that is no longer active. - if (!scriptId.empty()) + mRef = CellRef(state.mRef); + mData = RefData(state, mData.isDeletedByContentFile()); + + Ptr ptr(this); + + if (state.mHasLocals) { - if (const ESM::Script* script - = MWBase::Environment::get().getESMStore()->get().search(scriptId)) + const ESM::RefId& scriptId = mClass->getScript(ptr); + // Make sure we still have a script. It could have been coming from a content file that is no longer active. + if (!scriptId.empty()) { - try - { - mData.setLocals(*script); - mData.getLocals().read(state.mLocals, scriptId); - } - catch (const std::exception& exception) + if (const ESM::Script* script + = MWBase::Environment::get().getESMStore()->get().search(scriptId)) { - Log(Debug::Error) << "Error: failed to load state for local script " << scriptId - << " because an exception has been thrown: " << exception.what(); + try + { + mData.setLocals(*script); + mData.getLocals().read(state.mLocals, scriptId); + } + catch (const std::exception& exception) + { + Log(Debug::Error) << "Error: failed to load state for local script " << scriptId + << " because an exception has been thrown: " << exception.what(); + } } } } - } - mClass->readAdditionalState(ptr, state); + mClass->readAdditionalState(ptr, state); - if (!mRef.getSoul().empty() - && !MWBase::Environment::get().getESMStore()->get().search(mRef.getSoul())) - { - Log(Debug::Warning) << "Soul '" << mRef.getSoul() << "' not found, removing the soul from soul gem"; - mRef.setSoul(ESM::RefId()); - } + if (!mRef.getSoul().empty() + && !MWBase::Environment::get().getESMStore()->get().search(mRef.getSoul())) + { + Log(Debug::Warning) << "Soul '" << mRef.getSoul() << "' not found, removing the soul from soul gem"; + mRef.setSoul(ESM::RefId()); + } - MWBase::Environment::get().getLuaManager()->loadLocalScripts(ptr, state.mLuaScripts); -} + MWBase::Environment::get().getLuaManager()->loadLocalScripts(ptr, state.mLuaScripts); + } -void MWWorld::LiveCellRefBase::saveImp(ESM::ObjectState& state) const -{ - mRef.writeState(state); + void LiveCellRefBase::saveImp(ESM::ObjectState& state) const + { + mRef.writeState(state); - ConstPtr ptr(this); + ConstPtr ptr(this); - mData.write(state, mClass->getScript(ptr)); - MWBase::Environment::get().getLuaManager()->saveLocalScripts( - Ptr(const_cast(this)), state.mLuaScripts); + mData.write(state, mClass->getScript(ptr)); + MWBase::Environment::get().getLuaManager()->saveLocalScripts( + Ptr(const_cast(this)), state.mLuaScripts); - mClass->writeAdditionalState(ptr, state); -} + mClass->writeAdditionalState(ptr, state); + } -bool MWWorld::LiveCellRefBase::checkStateImp(const ESM::ObjectState& state) -{ - return true; -} + bool LiveCellRefBase::checkStateImp(const ESM::ObjectState& state) + { + return true; + } -unsigned int MWWorld::LiveCellRefBase::getType() const -{ - return mClass->getType(); -} + unsigned int LiveCellRefBase::getType() const + { + return mClass->getType(); + } -bool MWWorld::LiveCellRefBase::isDeleted() const -{ - return mData.isDeletedByContentFile() || mRef.getCount(false) == 0; -} + bool LiveCellRefBase::isDeleted() const + { + return mData.isDeletedByContentFile() || mRef.getCount(false) == 0; + } -namespace MWWorld -{ std::string makeDynamicCastErrorMessage(const LiveCellRefBase* value, std::string_view recordType) { std::stringstream message; diff --git a/apps/openmw/mwworld/livecellref.hpp b/apps/openmw/mwworld/livecellref.hpp index c95dd589b2..026d3edefd 100644 --- a/apps/openmw/mwworld/livecellref.hpp +++ b/apps/openmw/mwworld/livecellref.hpp @@ -17,6 +17,7 @@ namespace MWWorld class Ptr; class ESMStore; class Class; + class WorldModel; template struct LiveCellRef; @@ -29,17 +30,28 @@ namespace MWWorld /** Information about this instance, such as 3D location and rotation * and individual type-dependent data. */ - MWWorld::CellRef mRef; + CellRef mRef; /** runtime-data */ RefData mData; - LiveCellRefBase(unsigned int type, const ESM::CellRef& cref = ESM::CellRef()); + WorldModel* mWorldModel = nullptr; + + LiveCellRefBase(unsigned int type, const ESM::CellRef& cref); LiveCellRefBase(unsigned int type, const ESM4::Reference& cref); LiveCellRefBase(unsigned int type, const ESM4::ActorCharacter& cref); + + LiveCellRefBase(const LiveCellRefBase& other) = default; + + LiveCellRefBase(LiveCellRefBase&& other) noexcept; + /* Need this for the class to be recognized as polymorphic */ virtual ~LiveCellRefBase(); + LiveCellRefBase& operator=(const LiveCellRefBase& other) = default; + + LiveCellRefBase& operator=(LiveCellRefBase&& other) noexcept; + virtual void load(const ESM::ObjectState& state) = 0; ///< Load state into a LiveCellRef, that has already been initialised with base and class. /// @@ -132,12 +144,6 @@ namespace MWWorld { } - LiveCellRef(const X* b = nullptr) - : LiveCellRefBase(X::sRecordId) - , mBase(b) - { - } - // The object that this instance is based on. const X* mBase; diff --git a/apps/openmw/mwworld/player.cpp b/apps/openmw/mwworld/player.cpp index f5d38e8686..b776f27f06 100644 --- a/apps/openmw/mwworld/player.cpp +++ b/apps/openmw/mwworld/player.cpp @@ -36,8 +36,20 @@ namespace MWWorld { + namespace + { + ESM::CellRef makePlayerCellRef() + { + ESM::CellRef result; + result.blank(); + result.mRefID = ESM::RefId::stringRefId("Player"); + return result; + } + } + Player::Player(const ESM::NPC* player) - : mCellStore(nullptr) + : mPlayer(makePlayerCellRef(), player) + , mCellStore(nullptr) , mLastKnownExteriorPosition(0, 0, 0) , mMarkedPosition(ESM::Position()) , mMarkedCell(nullptr) @@ -46,11 +58,6 @@ namespace MWWorld , mPaidCrimeId(-1) , mJumping(false) { - ESM::CellRef cellRef; - cellRef.blank(); - cellRef.mRefID = ESM::RefId::stringRefId("Player"); - mPlayer = LiveCellRef(cellRef, player); - ESM::Position playerPos = mPlayer.mData.getPosition(); playerPos.pos[0] = playerPos.pos[1] = playerPos.pos[2] = 0; mPlayer.mData.setPosition(playerPos); diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index 66aa027a89..b95777ec76 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -451,8 +451,7 @@ namespace MWWorld } else if (!ESM::isEsm4Ext(worldspace)) { - static std::vector defaultHeight; - defaultHeight.resize(verts * verts, ESM::Land::DEFAULT_HEIGHT); + static const std::vector defaultHeight(verts * verts, ESM::Land::DEFAULT_HEIGHT); mPhysics->addHeightField(defaultHeight.data(), cellX, cellY, worldsize, verts, ESM::Land::DEFAULT_HEIGHT, ESM::Land::DEFAULT_HEIGHT, land.get()); } @@ -1126,19 +1125,19 @@ namespace MWWorld void Scene::preload(const std::string& mesh, bool useAnim) { - std::string meshPath = mesh; - if (useAnim) - meshPath = Misc::ResourceHelpers::correctActorModelPath(meshPath, mRendering.getResourceSystem()->getVFS()); + const VFS::Path::Normalized meshPath = useAnim + ? Misc::ResourceHelpers::correctActorModelPath(mesh, mRendering.getResourceSystem()->getVFS()) + : mesh; - if (!mRendering.getResourceSystem()->getSceneManager()->checkLoaded(meshPath, mRendering.getReferenceTime())) - { - osg::ref_ptr item(new PreloadMeshItem( - VFS::Path::toNormalized(meshPath), mRendering.getResourceSystem()->getSceneManager())); - mRendering.getWorkQueue()->addWorkItem(item); - const auto isDone = [](const osg::ref_ptr& v) { return v->isDone(); }; - mWorkItems.erase(std::remove_if(mWorkItems.begin(), mWorkItems.end(), isDone), mWorkItems.end()); - mWorkItems.emplace_back(std::move(item)); - } + if (mRendering.getResourceSystem()->getSceneManager()->checkLoaded(meshPath, mRendering.getReferenceTime())) + return; + + osg::ref_ptr item( + new PreloadMeshItem(meshPath, mRendering.getResourceSystem()->getSceneManager())); + mRendering.getWorkQueue()->addWorkItem(item); + const auto isDone = [](const osg::ref_ptr& v) { return v->isDone(); }; + mWorkItems.erase(std::remove_if(mWorkItems.begin(), mWorkItems.end(), isDone), mWorkItems.end()); + mWorkItems.emplace_back(std::move(item)); } void Scene::preloadCells(float dt) diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index b52d59d5e7..07da51b4f3 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -3644,10 +3644,10 @@ namespace MWWorld mRendering->spawnEffect(model, texture, worldPosition, 1.0f, false); } - void World::spawnEffect(const std::string& model, const std::string& textureOverride, const osg::Vec3f& worldPos, - float scale, bool isMagicVFX) + void World::spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride, + const osg::Vec3f& worldPos, float scale, bool isMagicVFX) { - mRendering->spawnEffect(VFS::Path::toNormalized(model), textureOverride, worldPos, scale, isMagicVFX); + mRendering->spawnEffect(model, textureOverride, worldPos, scale, isMagicVFX); } struct ResetActorsVisitor diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index f4c22e94d3..9e00118533 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "../mwbase/world.hpp" @@ -603,8 +604,8 @@ namespace MWWorld /// Spawn a blood effect for \a ptr at \a worldPosition void spawnBloodEffect(const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) override; - void spawnEffect(const std::string& model, const std::string& textureOverride, const osg::Vec3f& worldPos, - float scale = 1.f, bool isMagicVFX = true) override; + void spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride, + const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true) override; /// @see MWWorld::WeatherManager::isInStorm bool isInStorm() const override; diff --git a/apps/openmw/mwworld/worldmodel.cpp b/apps/openmw/mwworld/worldmodel.cpp index 3a1c486f0e..39ed27e96b 100644 --- a/apps/openmw/mwworld/worldmodel.cpp +++ b/apps/openmw/mwworld/worldmodel.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -339,6 +340,20 @@ namespace MWWorld throw std::runtime_error(std::string("Can't find cell with name ") + std::string(name)); return *result; } + + void WorldModel::registerPtr(const Ptr& ptr) + { + if (ptr.mRef == nullptr) + throw std::logic_error("Ptr with nullptr mRef is not allowed to be registered"); + mPtrRegistry.insert(ptr); + ptr.mRef->mWorldModel = this; + } + + void WorldModel::deregisterLiveCellRef(LiveCellRefBase& ref) noexcept + { + mPtrRegistry.remove(ref); + ref.mWorldModel = nullptr; + } } MWWorld::Ptr MWWorld::WorldModel::getPtrByRefId(const ESM::RefId& name) diff --git a/apps/openmw/mwworld/worldmodel.hpp b/apps/openmw/mwworld/worldmodel.hpp index 4c39d866de..e4b161d16e 100644 --- a/apps/openmw/mwworld/worldmodel.hpp +++ b/apps/openmw/mwworld/worldmodel.hpp @@ -77,9 +77,9 @@ namespace MWWorld std::size_t getPtrRegistryRevision() const { return mPtrRegistry.getRevision(); } - void registerPtr(const Ptr& ptr) { mPtrRegistry.insert(ptr); } + void registerPtr(const Ptr& ptr); - void deregisterLiveCellRef(const LiveCellRefBase& ref) noexcept { mPtrRegistry.remove(ref); } + void deregisterLiveCellRef(LiveCellRefBase& ref) noexcept; void assignSaveFileRefNum(ESM::CellRef& ref) { mPtrRegistry.assign(ref); } diff --git a/apps/openmw_tests/CMakeLists.txt b/apps/openmw_tests/CMakeLists.txt index 0c6b0d84df..9b57113110 100644 --- a/apps/openmw_tests/CMakeLists.txt +++ b/apps/openmw_tests/CMakeLists.txt @@ -9,6 +9,7 @@ file(GLOB UNITTEST_SRC_FILES mwworld/test_store.cpp mwworld/testduration.cpp mwworld/testtimestamp.cpp + mwworld/testptr.cpp mwdialogue/test_keywordsearch.cpp diff --git a/apps/openmw_tests/main.cpp b/apps/openmw_tests/main.cpp index fd7d4900c8..6b7298596a 100644 --- a/apps/openmw_tests/main.cpp +++ b/apps/openmw_tests/main.cpp @@ -1,11 +1,28 @@ #include +#include +#include +#include #include +#include + int main(int argc, char* argv[]) { Log::sMinDebugLevel = Debug::getDebugLevel(); + const std::filesystem::path settingsDefaultPath = std::filesystem::path{ OPENMW_PROJECT_SOURCE_DIR } / "files" + / Misc::StringUtils::stringToU8String("settings-default.cfg"); + + Settings::SettingsFileParser parser; + parser.loadSettingsFile(settingsDefaultPath, Settings::Manager::mDefaultSettings); + + Settings::StaticValues::initDefaults(); + + Settings::Manager::mUserSettings = Settings::Manager::mDefaultSettings; + + Settings::StaticValues::init(); + testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/apps/openmw_tests/mwworld/testptr.cpp b/apps/openmw_tests/mwworld/testptr.cpp new file mode 100644 index 0000000000..7bc0bebcec --- /dev/null +++ b/apps/openmw_tests/mwworld/testptr.cpp @@ -0,0 +1,82 @@ +#include "apps/openmw/mwclass/npc.hpp" +#include "apps/openmw/mwworld/esmstore.hpp" +#include "apps/openmw/mwworld/livecellref.hpp" +#include "apps/openmw/mwworld/ptr.hpp" +#include "apps/openmw/mwworld/worldmodel.hpp" + +#include +#include + +#include + +namespace MWWorld +{ + namespace + { + using namespace testing; + + TEST(MWWorldPtrTest, toStringShouldReturnHumanReadableTextRepresentationOfPtrWithNullRef) + { + Ptr ptr; + EXPECT_EQ(ptr.toString(), "null object"); + } + + TEST(MWWorldPtrTest, toStringShouldReturnHumanReadableTextRepresentationOfPtrWithDeletedRef) + { + MWClass::Npc::registerSelf(); + ESM::NPC npc; + npc.blank(); + npc.mId = ESM::RefId::stringRefId("Player"); + ESMStore store; + store.insert(npc); + ESM::CellRef cellRef; + cellRef.blank(); + cellRef.mRefID = npc.mId; + cellRef.mRefNum = ESM::RefNum{ .mIndex = 0x2a, .mContentFile = 0xd }; + LiveCellRef liveCellRef(cellRef, &npc); + liveCellRef.mData.setDeletedByContentFile(true); + Ptr ptr(&liveCellRef); + EXPECT_EQ(ptr.toString(), "deleted object0xd00002a (NPC, \"player\")"); + } + + TEST(MWWorldPtrTest, toStringShouldReturnHumanReadableTextRepresentationOfPtr) + { + MWClass::Npc::registerSelf(); + ESM::NPC npc; + npc.blank(); + npc.mId = ESM::RefId::stringRefId("Player"); + ESMStore store; + store.insert(npc); + ESM::CellRef cellRef; + cellRef.blank(); + cellRef.mRefID = npc.mId; + cellRef.mRefNum = ESM::RefNum{ .mIndex = 0x2a, .mContentFile = 0xd }; + LiveCellRef liveCellRef(cellRef, &npc); + Ptr ptr(&liveCellRef); + EXPECT_EQ(ptr.toString(), "object0xd00002a (NPC, \"player\")"); + } + + TEST(MWWorldPtrTest, underlyingLiveCellRefShouldBeDeregisteredOnDestruction) + { + MWClass::Npc::registerSelf(); + ESM::NPC npc; + npc.blank(); + npc.mId = ESM::RefId::stringRefId("Player"); + ESMStore store; + store.insert(npc); + ESM::ReadersCache readersCache; + WorldModel worldModel(store, readersCache); + ESM::CellRef cellRef; + cellRef.blank(); + cellRef.mRefID = npc.mId; + cellRef.mRefNum = ESM::FormId{ .mIndex = 0x2a, .mContentFile = 0xd }; + { + LiveCellRef liveCellRef(cellRef, &npc); + Ptr ptr(&liveCellRef); + worldModel.registerPtr(ptr); + ASSERT_EQ(worldModel.getPtr(cellRef.mRefNum), ptr); + } + EXPECT_EQ(worldModel.getPtr(cellRef.mRefNum), Ptr()); + } + } +} diff --git a/components/debug/gldebug.cpp b/components/debug/gldebug.cpp index 29036af162..482755b761 100644 --- a/components/debug/gldebug.cpp +++ b/components/debug/gldebug.cpp @@ -239,7 +239,7 @@ namespace Debug group->push(state); lastAppliedStack.push_back(group); } - if (!(lastAppliedStack.back() == this)) + if (lastAppliedStack.empty() || !(lastAppliedStack.back() == this)) { push(state); lastAppliedStack.push_back(this); diff --git a/components/esm/luascripts.cpp b/components/esm/luascripts.cpp index 71e2ce6dc1..81cc867aff 100644 --- a/components/esm/luascripts.cpp +++ b/components/esm/luascripts.cpp @@ -56,7 +56,7 @@ void ESM::LuaScriptsCfg::load(ESMReader& esm) { mScripts.emplace_back(); ESM::LuaScriptCfg& script = mScripts.back(); - script.mScriptPath = esm.getHString(); + script.mScriptPath = VFS::Path::Normalized(esm.getHString()); esm.getSubNameIs("LUAF"); esm.getSubHeader(); @@ -161,7 +161,7 @@ void ESM::LuaScripts::load(ESMReader& esm) { while (esm.isNextSub("LUAS")) { - std::string name = esm.getHString(); + VFS::Path::Normalized name(esm.getHString()); std::string data = loadLuaBinaryData(esm); std::vector timers; while (esm.isNextSub("LUAT")) diff --git a/components/esm3/cellref.cpp b/components/esm3/cellref.cpp index 97ccfb730a..927f517495 100644 --- a/components/esm3/cellref.cpp +++ b/components/esm3/cellref.cpp @@ -287,4 +287,10 @@ namespace ESM loadDataImpl(esm, isDeleted, cellRef); } + CellRef makeBlankCellRef() + { + CellRef result; + result.blank(); + return result; + } } diff --git a/components/esm3/cellref.hpp b/components/esm3/cellref.hpp index 8b23cc5c34..683bde200d 100644 --- a/components/esm3/cellref.hpp +++ b/components/esm3/cellref.hpp @@ -105,6 +105,8 @@ namespace ESM }; void skipLoadCellRef(ESMReader& esm, bool wideRefNum = false); + + CellRef makeBlankCellRef(); } #endif diff --git a/components/esm3/loadmgef.cpp b/components/esm3/loadmgef.cpp index 357dd94413..3ff725fcf7 100644 --- a/components/esm3/loadmgef.cpp +++ b/components/esm3/loadmgef.cpp @@ -132,53 +132,61 @@ namespace ESM esm.writeHNOString("DESC", mDescription); } - short MagicEffect::getResistanceEffect(short effect) + namespace { - // Source https://wiki.openmw.org/index.php?title=Research:Magic#Effect_attribute - - // - static std::map effects; - if (effects.empty()) + std::map makeEffectsMap() { - effects[DisintegrateArmor] = Sanctuary; - effects[DisintegrateWeapon] = Sanctuary; + std::map effects; - for (int i = DrainAttribute; i <= DamageSkill; ++i) - effects[i] = ResistMagicka; - for (int i = AbsorbAttribute; i <= AbsorbSkill; ++i) - effects[i] = ResistMagicka; - for (int i = WeaknessToFire; i <= WeaknessToNormalWeapons; ++i) - effects[i] = ResistMagicka; + effects[MagicEffect::Effects::DisintegrateArmor] = MagicEffect::Effects::Sanctuary; + effects[MagicEffect::Effects::DisintegrateWeapon] = MagicEffect::Effects::Sanctuary; + + for (int i = MagicEffect::Effects::DrainAttribute; i <= MagicEffect::Effects::DamageSkill; ++i) + effects[i] = MagicEffect::Effects::ResistMagicka; + for (int i = MagicEffect::Effects::AbsorbAttribute; i <= MagicEffect::Effects::AbsorbSkill; ++i) + effects[i] = MagicEffect::Effects::ResistMagicka; + for (int i = MagicEffect::Effects::WeaknessToFire; i <= MagicEffect::Effects::WeaknessToNormalWeapons; ++i) + effects[i] = MagicEffect::Effects::ResistMagicka; - effects[Burden] = ResistMagicka; - effects[Charm] = ResistMagicka; - effects[Silence] = ResistMagicka; - effects[Blind] = ResistMagicka; - effects[Sound] = ResistMagicka; + effects[MagicEffect::Effects::Burden] = MagicEffect::Effects::ResistMagicka; + effects[MagicEffect::Effects::Charm] = MagicEffect::Effects::ResistMagicka; + effects[MagicEffect::Effects::Silence] = MagicEffect::Effects::ResistMagicka; + effects[MagicEffect::Effects::Blind] = MagicEffect::Effects::ResistMagicka; + effects[MagicEffect::Effects::Sound] = MagicEffect::Effects::ResistMagicka; for (int i = 0; i < 2; ++i) { - effects[CalmHumanoid + i] = ResistMagicka; - effects[FrenzyHumanoid + i] = ResistMagicka; - effects[DemoralizeHumanoid + i] = ResistMagicka; - effects[RallyHumanoid + i] = ResistMagicka; + effects[MagicEffect::Effects::CalmHumanoid + i] = MagicEffect::Effects::ResistMagicka; + effects[MagicEffect::Effects::FrenzyHumanoid + i] = MagicEffect::Effects::ResistMagicka; + effects[MagicEffect::Effects::DemoralizeHumanoid + i] = MagicEffect::Effects::ResistMagicka; + effects[MagicEffect::Effects::RallyHumanoid + i] = MagicEffect::Effects::ResistMagicka; } - effects[TurnUndead] = ResistMagicka; + effects[MagicEffect::Effects::TurnUndead] = MagicEffect::Effects::ResistMagicka; + + effects[MagicEffect::Effects::FireDamage] = MagicEffect::Effects::ResistFire; + effects[MagicEffect::Effects::FrostDamage] = MagicEffect::Effects::ResistFrost; + effects[MagicEffect::Effects::ShockDamage] = MagicEffect::Effects::ResistShock; + effects[MagicEffect::Effects::Vampirism] = MagicEffect::Effects::ResistCommonDisease; + effects[MagicEffect::Effects::Corprus] = MagicEffect::Effects::ResistCorprusDisease; + effects[MagicEffect::Effects::Poison] = MagicEffect::Effects::ResistPoison; + effects[MagicEffect::Effects::Paralyze] = MagicEffect::Effects::ResistParalysis; - effects[FireDamage] = ResistFire; - effects[FrostDamage] = ResistFrost; - effects[ShockDamage] = ResistShock; - effects[Vampirism] = ResistCommonDisease; - effects[Corprus] = ResistCorprusDisease; - effects[Poison] = ResistPoison; - effects[Paralyze] = ResistParalysis; + return effects; } + } - if (effects.find(effect) != effects.end()) - return effects[effect]; - else - return -1; + short MagicEffect::getResistanceEffect(short effect) + { + // Source https://wiki.openmw.org/index.php?title=Research:Magic#Effect_attribute + + // + static const std::map effects = makeEffectsMap(); + + if (const auto it = effects.find(effect); it != effects.end()) + return it->second; + + return -1; } short MagicEffect::getWeaknessEffect(short effect) diff --git a/components/esm4/reader.cpp b/components/esm4/reader.cpp index 2d9a929bb2..505922601d 100644 --- a/components/esm4/reader.cpp +++ b/components/esm4/reader.cpp @@ -320,16 +320,18 @@ namespace ESM4 std::filesystem::path path = strings / (prefix + language + suffix); if (mVFS != nullptr) { - std::string vfsPath = Files::pathToUnicodeString(path); - if (!mVFS->exists(vfsPath)) + VFS::Path::Normalized vfsPath(Files::pathToUnicodeString(path)); + Files::IStreamPtr stream = mVFS->find(vfsPath); + + if (stream == nullptr) { path = strings / (prefix + altLanguage + suffix); - vfsPath = Files::pathToUnicodeString(path); + vfsPath = VFS::Path::Normalized(Files::pathToUnicodeString(path)); + stream = mVFS->find(vfsPath); } - if (mVFS->exists(vfsPath)) + if (stream != nullptr) { - const Files::IStreamPtr stream = mVFS->get(vfsPath); buildLStringIndex(stringType, *stream); return; } diff --git a/components/lua_ui/element.cpp b/components/lua_ui/element.cpp index f3f873a583..2d8462e273 100644 --- a/components/lua_ui/element.cpp +++ b/components/lua_ui/element.cpp @@ -113,6 +113,7 @@ namespace LuaUi ContentView content(LuaUtil::cast(contentObj)); result.resize(content.size()); size_t minSize = std::min(children.size(), content.size()); + std::vector toDestroy; for (size_t i = 0; i < minSize; i++) { WidgetExtension* ext = children[i]; @@ -121,7 +122,7 @@ namespace LuaUi { WidgetExtension* root = pluckElementRoot(child, depth); if (ext != root) - destroyChild(ext); + toDestroy.emplace_back(ext); result[i] = root; } else @@ -133,14 +134,12 @@ namespace LuaUi } else { - destroyChild(ext); + toDestroy.emplace_back(ext); ext = createWidget(newLayout, false, depth); } result[i] = ext; } } - for (size_t i = minSize; i < children.size(); i++) - destroyChild(children[i]); for (size_t i = minSize; i < content.size(); i++) { sol::object child = content.at(i); @@ -149,6 +148,11 @@ namespace LuaUi else result[i] = createWidget(child.as(), false, depth); } + // Don't destroy anything until element creation has had a chance to throw + for (size_t i = minSize; i < children.size(); i++) + destroyChild(children[i]); + for (WidgetExtension* ext : toDestroy) + destroyChild(ext); return result; } @@ -217,7 +221,9 @@ namespace LuaUi std::string setLayer(WidgetExtension* ext, const sol::table& layout) { MyGUI::ILayer* layerNode = ext->widget()->getLayer(); - std::string currentLayer = layerNode ? layerNode->getName() : std::string(); + std::string_view currentLayer; + if (layerNode) + currentLayer = layerNode->getName(); std::string newLayer = layout.get_or(LayoutKeys::layer, std::string()); if (!newLayer.empty() && !MyGUI::LayerManager::getInstance().isExist(newLayer)) throw std::logic_error(std::string("Layer ") + newLayer + " doesn't exist"); @@ -278,9 +284,20 @@ namespace LuaUi WidgetExtension* parent = mRoot->getParent(); auto children = parent->children(); auto it = std::find(children.begin(), children.end(), mRoot); - mRoot = createWidget(layout(), true, 0); assert(it != children.end()); - *it = mRoot; + try + { + mRoot = createWidget(layout(), true, 0); + *it = mRoot; + } + catch (...) + { + // Remove mRoot from its parent's children even if we couldn't replace it + children.erase(it); + parent->setChildren(children); + mRoot = nullptr; + throw; + } parent->setChildren(children); mRoot->updateCoord(); } @@ -300,6 +317,10 @@ namespace LuaUi { if (mRoot != nullptr) { + // If someone decided to destroy an element used as another element's content, we need to detach it + // first so the parent doesn't end up holding a stale pointer + if (WidgetExtension* parent = mRoot->getParent()) + parent->detachChildrenIf([&](WidgetExtension* child) { return child == mRoot; }); destroyRoot(mRoot); mRoot = nullptr; } diff --git a/components/lua_ui/widget.cpp b/components/lua_ui/widget.cpp index e7e1053ab7..71416be8c8 100644 --- a/components/lua_ui/widget.cpp +++ b/components/lua_ui/widget.cpp @@ -126,6 +126,7 @@ namespace LuaUi { mParent = nullptr; widget()->detachFromWidget(); + widget()->detachFromLayer(); } WidgetExtension* WidgetExtension::findDeep(std::string_view flagName) diff --git a/components/lua_ui/widget.hpp b/components/lua_ui/widget.hpp index 24962f6820..0ec688d3bb 100644 --- a/components/lua_ui/widget.hpp +++ b/components/lua_ui/widget.hpp @@ -179,7 +179,7 @@ namespace LuaUi void updateVisible(); - void detachChildrenIf(auto&& predicate, std::vector children) + void detachChildrenIf(auto&& predicate, std::vector& children) { for (auto it = children.begin(); it != children.end();) { diff --git a/components/misc/resourcehelpers.cpp b/components/misc/resourcehelpers.cpp index 5ed0bcc972..1728e51c7a 100644 --- a/components/misc/resourcehelpers.cpp +++ b/components/misc/resourcehelpers.cpp @@ -228,7 +228,7 @@ bool Misc::ResourceHelpers::isHiddenMarker(const ESM::RefId& id) namespace { - std::string getLODMeshNameImpl(std::string resPath, const VFS::Manager* vfs, std::string_view pattern) + std::string getLODMeshNameImpl(std::string resPath, std::string_view pattern) { if (auto w = Misc::findExtension(resPath); w != std::string::npos) resPath.insert(w, pattern); @@ -237,7 +237,7 @@ namespace std::string getBestLODMeshName(std::string const& resPath, const VFS::Manager* vfs, std::string_view pattern) { - if (const auto& result = getLODMeshNameImpl(resPath, vfs, pattern); vfs->exists(result)) + if (std::string result = getLODMeshNameImpl(resPath, pattern); vfs->exists(result)) return result; return resPath; } diff --git a/components/myguiplatform/myguitexture.cpp b/components/myguiplatform/myguitexture.cpp index fe2b700049..9d865e1296 100644 --- a/components/myguiplatform/myguitexture.cpp +++ b/components/myguiplatform/myguitexture.cpp @@ -95,7 +95,7 @@ namespace osgMyGUI if (!mImageManager) throw std::runtime_error("No imagemanager set"); - osg::ref_ptr image(mImageManager->getImage(VFS::Path::toNormalized(fname))); + osg::ref_ptr image(mImageManager->getImage(VFS::Path::Normalized(fname))); mTexture = new osg::Texture2D(image); mTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); mTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index bcda7efe70..6bf6ad89bb 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -53,7 +53,7 @@ namespace NifBullet mShape->mFileName = nif.getFilename(); if (roots.empty()) { - warn("Found no root nodes in NIF file " + mShape->mFileName); + warn("Found no root nodes in NIF file " + mShape->mFileName.value()); return mShape; } @@ -93,7 +93,7 @@ namespace NifBullet } else { - warn("Invalid Bounding Box node bounds in file " + mShape->mFileName); + warn("Invalid Bounding Box node bounds in file " + mShape->mFileName.value()); } return true; } diff --git a/components/resource/bulletshape.hpp b/components/resource/bulletshape.hpp index 0a1b98bf7c..d9aad0f092 100644 --- a/components/resource/bulletshape.hpp +++ b/components/resource/bulletshape.hpp @@ -1,7 +1,6 @@ #ifndef OPENMW_COMPONENTS_RESOURCE_BULLETSHAPE_H #define OPENMW_COMPONENTS_RESOURCE_BULLETSHAPE_H -#include #include #include @@ -12,6 +11,8 @@ #include #include +#include + class btCollisionShape; namespace NifBullet @@ -56,7 +57,7 @@ namespace Resource // we store the node's record index mapped to the child index of the shape in the btCompoundShape. std::map mAnimatedShapes; - std::string mFileName; + VFS::Path::Normalized mFileName; std::string mFileHash; VisualCollisionType mVisualCollisionType = VisualCollisionType::None; diff --git a/components/resource/bulletshapemanager.cpp b/components/resource/bulletshapemanager.cpp index 93c53d8cb0..345efc7fbd 100644 --- a/components/resource/bulletshapemanager.cpp +++ b/components/resource/bulletshapemanager.cpp @@ -106,93 +106,83 @@ namespace Resource { } - BulletShapeManager::~BulletShapeManager() {} + BulletShapeManager::~BulletShapeManager() = default; - osg::ref_ptr BulletShapeManager::getShape(const std::string& name) + osg::ref_ptr BulletShapeManager::getShape(VFS::Path::NormalizedView name) { - const VFS::Path::Normalized normalized(name); + if (osg::ref_ptr obj = mCache->getRefFromObjectCache(name)) + return osg::ref_ptr(static_cast(obj.get())); osg::ref_ptr shape; - osg::ref_ptr obj = mCache->getRefFromObjectCache(normalized); - if (obj) - shape = osg::ref_ptr(static_cast(obj.get())); + + if (Misc::getFileExtension(name.value()) == "nif") + { + NifBullet::BulletNifLoader loader; + shape = loader.load(*mNifFileManager->get(name)); + } else { - if (Misc::getFileExtension(normalized) == "nif") + // TODO: support .bullet shape files + + osg::ref_ptr constNode(mSceneManager->getTemplate(name)); + // const-trickery required because there is no const version of NodeVisitor + osg::ref_ptr node(const_cast(constNode.get())); + + // Check first if there's a custom collision node + unsigned int visitAllNodesMask = 0xffffffff; + SceneUtil::FindByNameVisitor nameFinder("Collision"); + nameFinder.setTraversalMask(visitAllNodesMask); + nameFinder.setNodeMaskOverride(visitAllNodesMask); + node->accept(nameFinder); + if (nameFinder.mFoundNode) { - NifBullet::BulletNifLoader loader; - shape = loader.load(*mNifFileManager->get(normalized)); + NodeToShapeVisitor visitor; + visitor.setTraversalMask(visitAllNodesMask); + visitor.setNodeMaskOverride(visitAllNodesMask); + nameFinder.mFoundNode->accept(visitor); + shape = visitor.getShape(); } - else + + // Generate a collision shape from the mesh + if (!shape) { - // TODO: support .bullet shape files - - osg::ref_ptr constNode(mSceneManager->getTemplate(normalized)); - osg::ref_ptr node(const_cast( - constNode.get())); // const-trickery required because there is no const version of NodeVisitor - - // Check first if there's a custom collision node - unsigned int visitAllNodesMask = 0xffffffff; - SceneUtil::FindByNameVisitor nameFinder("Collision"); - nameFinder.setTraversalMask(visitAllNodesMask); - nameFinder.setNodeMaskOverride(visitAllNodesMask); - node->accept(nameFinder); - if (nameFinder.mFoundNode) - { - NodeToShapeVisitor visitor; - visitor.setTraversalMask(visitAllNodesMask); - visitor.setNodeMaskOverride(visitAllNodesMask); - nameFinder.mFoundNode->accept(visitor); - shape = visitor.getShape(); - } - - // Generate a collision shape from the mesh + NodeToShapeVisitor visitor; + node->accept(visitor); + shape = visitor.getShape(); if (!shape) - { - NodeToShapeVisitor visitor; - node->accept(visitor); - shape = visitor.getShape(); - if (!shape) - return osg::ref_ptr(); - } - - if (shape != nullptr) - { - shape->mFileName = normalized; - constNode->getUserValue(Misc::OsgUserValues::sFileHash, shape->mFileHash); - } + return osg::ref_ptr(); } - mCache->addEntryToObjectCache(normalized, shape); + if (shape != nullptr) + { + shape->mFileName = name; + constNode->getUserValue(Misc::OsgUserValues::sFileHash, shape->mFileHash); + } } + + mCache->addEntryToObjectCache(name.value(), shape); + return shape; } - osg::ref_ptr BulletShapeManager::cacheInstance(const std::string& name) + osg::ref_ptr BulletShapeManager::cacheInstance(VFS::Path::NormalizedView name) { - const std::string normalized = VFS::Path::normalizeFilename(name); - - osg::ref_ptr instance = createInstance(normalized); - if (instance) - mInstanceCache->addEntryToObjectCache(normalized, instance.get()); + osg::ref_ptr instance = createInstance(name); + if (instance != nullptr) + mInstanceCache->addEntryToObjectCache(name, instance.get()); return instance; } - osg::ref_ptr BulletShapeManager::getInstance(const std::string& name) + osg::ref_ptr BulletShapeManager::getInstance(VFS::Path::NormalizedView name) { - const std::string normalized = VFS::Path::normalizeFilename(name); - - osg::ref_ptr obj = mInstanceCache->takeFromObjectCache(normalized); - if (obj.get()) + if (osg::ref_ptr obj = mInstanceCache->takeFromObjectCache(name)) return static_cast(obj.get()); - else - return createInstance(normalized); + return createInstance(name); } - osg::ref_ptr BulletShapeManager::createInstance(const std::string& name) + osg::ref_ptr BulletShapeManager::createInstance(VFS::Path::NormalizedView name) { - osg::ref_ptr shape = getShape(name); - if (shape) + if (osg::ref_ptr shape = getShape(name)) return makeInstance(std::move(shape)); return osg::ref_ptr(); } diff --git a/components/resource/bulletshapemanager.hpp b/components/resource/bulletshapemanager.hpp index a81296fdfd..3c5a495c77 100644 --- a/components/resource/bulletshapemanager.hpp +++ b/components/resource/bulletshapemanager.hpp @@ -1,11 +1,10 @@ #ifndef OPENMW_COMPONENTS_BULLETSHAPEMANAGER_H #define OPENMW_COMPONENTS_BULLETSHAPEMANAGER_H -#include -#include - #include +#include + #include "bulletshape.hpp" #include "resourcemanager.hpp" @@ -30,16 +29,16 @@ namespace Resource ~BulletShapeManager(); /// @note May return a null pointer if the object has no shape. - osg::ref_ptr getShape(const std::string& name); + osg::ref_ptr getShape(VFS::Path::NormalizedView name); /// Create an instance of the given shape and cache it for later use, so that future calls to getInstance() can /// simply return the cached instance instead of having to create a new one. /// @note The returned ref_ptr may be kept by the caller to ensure that the instance stays in cache for as long /// as needed. - osg::ref_ptr cacheInstance(const std::string& name); + osg::ref_ptr cacheInstance(VFS::Path::NormalizedView name); /// @note May return a null pointer if the object has no shape. - osg::ref_ptr getInstance(const std::string& name); + osg::ref_ptr getInstance(VFS::Path::NormalizedView name); /// @see ResourceManager::updateCache void updateCache(double referenceTime) override; @@ -49,7 +48,7 @@ namespace Resource void reportStats(unsigned int frameNumber, osg::Stats* stats) const override; private: - osg::ref_ptr createInstance(const std::string& name); + osg::ref_ptr createInstance(VFS::Path::NormalizedView name); osg::ref_ptr mInstanceCache; SceneManager* mSceneManager; diff --git a/components/resource/foreachbulletobject.cpp b/components/resource/foreachbulletobject.cpp index b936e39a2a..9c205c8228 100644 --- a/components/resource/foreachbulletobject.cpp +++ b/components/resource/foreachbulletobject.cpp @@ -16,10 +16,6 @@ #include #include -#include -#include -#include -#include #include #include @@ -97,7 +93,7 @@ namespace Resource for (CellRef& cellRef : cellRefs) { - std::string model(getModel(esmData, cellRef.mRefId, cellRef.mType)); + VFS::Path::Normalized model(getModel(esmData, cellRef.mRefId, cellRef.mType)); if (model.empty()) continue; @@ -107,7 +103,8 @@ namespace Resource osg::ref_ptr shape = [&] { try { - return bulletShapeManager.getShape("meshes/" + model); + constexpr VFS::Path::NormalizedView prefix("meshes"); + return bulletShapeManager.getShape(prefix / model); } catch (const std::exception& e) { diff --git a/components/resource/multiobjectcache.cpp b/components/resource/multiobjectcache.cpp index 71500b0ceb..0be9be232e 100644 --- a/components/resource/multiobjectcache.cpp +++ b/components/resource/multiobjectcache.cpp @@ -6,11 +6,6 @@ namespace Resource { - - MultiObjectCache::MultiObjectCache() {} - - MultiObjectCache::~MultiObjectCache() {} - void MultiObjectCache::removeUnreferencedObjectsInCache() { std::vector> objectsToRemove; @@ -44,7 +39,7 @@ namespace Resource _objectCache.clear(); } - void MultiObjectCache::addEntryToObjectCache(const std::string& filename, osg::Object* object) + void MultiObjectCache::addEntryToObjectCache(VFS::Path::NormalizedView filename, osg::Object* object) { if (!object) { @@ -52,23 +47,23 @@ namespace Resource return; } std::lock_guard lock(_objectCacheMutex); - _objectCache.insert(std::make_pair(filename, object)); + _objectCache.emplace(filename, object); } - osg::ref_ptr MultiObjectCache::takeFromObjectCache(const std::string& fileName) + osg::ref_ptr MultiObjectCache::takeFromObjectCache(VFS::Path::NormalizedView fileName) { std::lock_guard lock(_objectCacheMutex); ++mGet; - ObjectCacheMap::iterator found = _objectCache.find(fileName); - if (found == _objectCache.end()) - return osg::ref_ptr(); - else + const auto it = _objectCache.find(fileName); + if (it != _objectCache.end()) { - osg::ref_ptr object = std::move(found->second); - _objectCache.erase(found); + osg::ref_ptr object = std::move(it->second); + _objectCache.erase(it); ++mHit; return object; } + + return nullptr; } void MultiObjectCache::releaseGLObjects(osg::State* state) diff --git a/components/resource/multiobjectcache.hpp b/components/resource/multiobjectcache.hpp index 654a88b524..6e05f462f0 100644 --- a/components/resource/multiobjectcache.hpp +++ b/components/resource/multiobjectcache.hpp @@ -3,11 +3,12 @@ #include #include -#include #include #include +#include + #include "cachestats.hpp" namespace osg @@ -23,18 +24,15 @@ namespace Resource class MultiObjectCache : public osg::Referenced { public: - MultiObjectCache(); - ~MultiObjectCache(); - void removeUnreferencedObjectsInCache(); /** Remove all objects from the cache. */ void clear(); - void addEntryToObjectCache(const std::string& filename, osg::Object* object); + void addEntryToObjectCache(VFS::Path::NormalizedView filename, osg::Object* object); /** Take an Object from cache. Return nullptr if no object found. */ - osg::ref_ptr takeFromObjectCache(const std::string& fileName); + osg::ref_ptr takeFromObjectCache(VFS::Path::NormalizedView fileName); /** call releaseGLObjects on all objects attached to the object cache.*/ void releaseGLObjects(osg::State* state); @@ -42,7 +40,7 @@ namespace Resource CacheStats getStats() const; protected: - typedef std::multimap> ObjectCacheMap; + typedef std::multimap, std::less<>> ObjectCacheMap; ObjectCacheMap _objectCache; mutable std::mutex _objectCacheMutex; diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index 9feab669a9..82a77ccc0a 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -597,9 +597,9 @@ namespace Resource mShaderManager->setShaderPath(path); } - bool SceneManager::checkLoaded(const std::string& name, double timeStamp) + bool SceneManager::checkLoaded(VFS::Path::NormalizedView name, double timeStamp) { - return mCache->checkInObjectCache(VFS::Path::normalizeFilename(name), timeStamp); + return mCache->checkInObjectCache(name, timeStamp); } void SceneManager::setUpNormalsRTForStateSet(osg::StateSet* stateset, bool enabled) diff --git a/components/resource/scenemanager.hpp b/components/resource/scenemanager.hpp index 6ea94233e7..ecd94e257c 100644 --- a/components/resource/scenemanager.hpp +++ b/components/resource/scenemanager.hpp @@ -152,7 +152,7 @@ namespace Resource void setShaderPath(const std::filesystem::path& path); /// Check if a given scene is loaded and if so, update its usage timestamp to prevent it from being unloaded - bool checkLoaded(const std::string& name, double referenceTime); + bool checkLoaded(VFS::Path::NormalizedView name, double referenceTime); /// Get a read-only copy of this scene "template" /// @note If the given filename does not exist or fails to load, an error marker mesh will be used instead. diff --git a/components/sceneutil/attach.cpp b/components/sceneutil/attach.cpp index 1c21221ac4..7b43380c98 100644 --- a/components/sceneutil/attach.cpp +++ b/components/sceneutil/attach.cpp @@ -82,18 +82,32 @@ namespace SceneUtil std::string_view mFilter; }; - void mergeUserData(const osg::UserDataContainer* source, osg::Object* target) + namespace { - if (!source) - return; - if (!target->getUserDataContainer()) - target->setUserDataContainer(osg::clone(source, osg::CopyOp::SHALLOW_COPY)); - else + void mergeUserData(const osg::UserDataContainer* source, osg::Object* target) { - for (unsigned int i = 0; i < source->getNumUserObjects(); ++i) - target->getUserDataContainer()->addUserObject( - osg::clone(source->getUserObject(i), osg::CopyOp::SHALLOW_COPY)); + if (!source) + return; + + if (!target->getUserDataContainer()) + target->setUserDataContainer(osg::clone(source, osg::CopyOp::SHALLOW_COPY)); + else + { + for (unsigned int i = 0; i < source->getNumUserObjects(); ++i) + target->getUserDataContainer()->addUserObject( + osg::clone(source->getUserObject(i), osg::CopyOp::SHALLOW_COPY)); + } + } + + osg::ref_ptr makeFrontFaceStateSet() + { + osg::ref_ptr frontFace = new osg::FrontFace; + frontFace->setMode(osg::FrontFace::CLOCKWISE); + + osg::ref_ptr frontFaceStateSet = new osg::StateSet; + frontFaceStateSet->setAttributeAndModes(frontFace, osg::StateAttribute::ON); + return frontFaceStateSet; } } @@ -159,14 +173,8 @@ namespace SceneUtil // Note: for absolute correctness we would need to check the current front face for every mesh then // invert it However MW isn't doing this either, so don't. Assuming all meshes are using backface // culling is more efficient. - static osg::ref_ptr frontFaceStateSet; - if (!frontFaceStateSet) - { - frontFaceStateSet = new osg::StateSet; - osg::FrontFace* frontFace = new osg::FrontFace; - frontFace->setMode(osg::FrontFace::CLOCKWISE); - frontFaceStateSet->setAttributeAndModes(frontFace, osg::StateAttribute::ON); - } + static const osg::ref_ptr frontFaceStateSet = makeFrontFaceStateSet(); + trans->setStateSet(frontFaceStateSet); } diff --git a/components/settings/categories/gui.hpp b/components/settings/categories/gui.hpp index 4a5e50fd8a..a26364c5dd 100644 --- a/components/settings/categories/gui.hpp +++ b/components/settings/categories/gui.hpp @@ -33,7 +33,11 @@ namespace Settings SettingValue mKeyboardNavigation{ mIndex, "GUI", "keyboard navigation" }; SettingValue mColorTopicEnable{ mIndex, "GUI", "color topic enable" }; SettingValue mColorTopicSpecific{ mIndex, "GUI", "color topic specific" }; + SettingValue mColorTopicSpecificOver{ mIndex, "GUI", "color topic specific over" }; + SettingValue mColorTopicSpecificPressed{ mIndex, "GUI", "color topic specific pressed" }; SettingValue mColorTopicExhausted{ mIndex, "GUI", "color topic exhausted" }; + SettingValue mColorTopicExhaustedOver{ mIndex, "GUI", "color topic exhausted over" }; + SettingValue mColorTopicExhaustedPressed{ mIndex, "GUI", "color topic exhausted pressed" }; }; } diff --git a/components/terrain/defs.hpp b/components/terrain/defs.hpp index c2342c50d2..48e4813e65 100644 --- a/components/terrain/defs.hpp +++ b/components/terrain/defs.hpp @@ -1,7 +1,7 @@ #ifndef COMPONENTS_TERRAIN_DEFS_HPP #define COMPONENTS_TERRAIN_DEFS_HPP -#include +#include namespace Terrain { @@ -16,8 +16,8 @@ namespace Terrain struct LayerInfo { - std::string mDiffuseMap; - std::string mNormalMap; + VFS::Path::Normalized mDiffuseMap; + VFS::Path::Normalized mNormalMap; bool mParallax; // Height info in normal map alpha channel? bool mSpecular; // Specular info in diffuse map alpha channel? diff --git a/components/terrain/quadtreeworld.cpp b/components/terrain/quadtreeworld.cpp index 63b55abb21..99938d7b3a 100644 --- a/components/terrain/quadtreeworld.cpp +++ b/components/terrain/quadtreeworld.cpp @@ -404,63 +404,68 @@ namespace Terrain } } - void updateWaterCullingView( - HeightCullCallback* callback, ViewData* vd, osgUtil::CullVisitor* cv, float cellworldsize, bool outofworld) + namespace { - if (!(cv->getTraversalMask() & callback->getCullMask())) - return; - float lowZ = std::numeric_limits::max(); - float highZ = callback->getHighZ(); - if (cv->getEyePoint().z() <= highZ || outofworld) + osg::ref_ptr makeStateSet() { - callback->setLowZ(-std::numeric_limits::max()); - return; + osg::ref_ptr stateSet = new osg::StateSet; + stateSet->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); + stateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + stateSet->setAttributeAndModes( + new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE), + osg::StateAttribute::ON); + osg::ref_ptr material = new osg::Material; + material->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 1, 1)); + material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, 1)); + material->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, 1)); + stateSet->setAttributeAndModes(material, osg::StateAttribute::ON); + stateSet->setRenderBinDetails(100, "RenderBin"); + return stateSet; } - cv->pushCurrentMask(); - static bool debug = getenv("OPENMW_WATER_CULLING_DEBUG") != nullptr; - for (unsigned int i = 0; i < vd->getNumEntries(); ++i) + + void updateWaterCullingView( + HeightCullCallback* callback, ViewData* vd, osgUtil::CullVisitor* cv, float cellworldsize, bool outofworld) { - ViewDataEntry& entry = vd->getEntry(i); - osg::BoundingBox bb - = static_cast(entry.mRenderingNode->asGroup()->getChild(0))->getWaterBoundingBox(); - if (!bb.valid()) - continue; - osg::Vec3f ofs( - entry.mNode->getCenter().x() * cellworldsize, entry.mNode->getCenter().y() * cellworldsize, 0.f); - bb._min += ofs; - bb._max += ofs; - bb._min.z() = highZ; - bb._max.z() = highZ; - if (cv->isCulled(bb)) - continue; - lowZ = bb._min.z(); - - if (!debug) - break; - osg::Box* b = new osg::Box; - b->set(bb.center(), bb._max - bb.center()); - osg::ShapeDrawable* drw = new osg::ShapeDrawable(b); - static osg::ref_ptr stateset = nullptr; - if (!stateset) + if (!(cv->getTraversalMask() & callback->getCullMask())) + return; + float lowZ = std::numeric_limits::max(); + float highZ = callback->getHighZ(); + if (cv->getEyePoint().z() <= highZ || outofworld) { - stateset = new osg::StateSet; - stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); - stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); - stateset->setAttributeAndModes( - new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE), - osg::StateAttribute::ON); - osg::Material* m = new osg::Material; - m->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 1, 1)); - m->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, 1)); - m->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, 1)); - stateset->setAttributeAndModes(m, osg::StateAttribute::ON); - stateset->setRenderBinDetails(100, "RenderBin"); + callback->setLowZ(-std::numeric_limits::max()); + return; + } + cv->pushCurrentMask(); + static bool debug = getenv("OPENMW_WATER_CULLING_DEBUG") != nullptr; + for (unsigned int i = 0; i < vd->getNumEntries(); ++i) + { + ViewDataEntry& entry = vd->getEntry(i); + osg::BoundingBox bb = static_cast(entry.mRenderingNode->asGroup()->getChild(0)) + ->getWaterBoundingBox(); + if (!bb.valid()) + continue; + osg::Vec3f ofs( + entry.mNode->getCenter().x() * cellworldsize, entry.mNode->getCenter().y() * cellworldsize, 0.f); + bb._min += ofs; + bb._max += ofs; + bb._min.z() = highZ; + bb._max.z() = highZ; + if (cv->isCulled(bb)) + continue; + lowZ = bb._min.z(); + + if (!debug) + break; + osg::Box* b = new osg::Box; + b->set(bb.center(), bb._max - bb.center()); + osg::ShapeDrawable* drw = new osg::ShapeDrawable(b); + static const osg::ref_ptr stateset = makeStateSet(); + drw->setStateSet(stateset); + drw->accept(*cv); } - drw->setStateSet(stateset); - drw->accept(*cv); + callback->setLowZ(lowZ); + cv->popCurrentMask(); } - callback->setLowZ(lowZ); - cv->popCurrentMask(); } void QuadTreeWorld::accept(osg::NodeVisitor& nv) diff --git a/components/terrain/texturemanager.cpp b/components/terrain/texturemanager.cpp index 8df880fab0..06f3243330 100644 --- a/components/terrain/texturemanager.cpp +++ b/components/terrain/texturemanager.cpp @@ -35,23 +35,21 @@ namespace Terrain mCache->call(f); } - osg::ref_ptr TextureManager::getTexture(const std::string& name) + osg::ref_ptr TextureManager::getTexture(VFS::Path::NormalizedView name) { // don't bother with case folding, since there is only one way of referring to terrain textures we can assume // the case is always the same osg::ref_ptr obj = mCache->getRefFromObjectCache(name); - if (obj) + + if (obj != nullptr) return static_cast(obj.get()); - else - { - osg::ref_ptr texture( - new osg::Texture2D(mSceneManager->getImageManager()->getImage(VFS::Path::toNormalized(name)))); - texture->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); - texture->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); - mSceneManager->applyFilterSettings(texture); - mCache->addEntryToObjectCache(name, texture.get()); - return texture; - } + + osg::ref_ptr texture(new osg::Texture2D(mSceneManager->getImageManager()->getImage(name))); + texture->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); + texture->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); + mSceneManager->applyFilterSettings(texture); + mCache->addEntryToObjectCache(name.value(), texture.get()); + return texture; } void TextureManager::reportStats(unsigned int frameNumber, osg::Stats* stats) const diff --git a/components/terrain/texturemanager.hpp b/components/terrain/texturemanager.hpp index 96fb43abfa..a79ef53b88 100644 --- a/components/terrain/texturemanager.hpp +++ b/components/terrain/texturemanager.hpp @@ -1,9 +1,8 @@ #ifndef OPENMW_COMPONENTS_TERRAIN_TEXTUREMANAGER_H #define OPENMW_COMPONENTS_TERRAIN_TEXTUREMANAGER_H -#include - #include +#include namespace Resource { @@ -25,7 +24,7 @@ namespace Terrain void updateTextureFiltering(); - osg::ref_ptr getTexture(const std::string& name); + osg::ref_ptr getTexture(VFS::Path::NormalizedView name); void reportStats(unsigned int frameNumber, osg::Stats* stats) const override; diff --git a/docs/source/reference/modding/settings/GUI.rst b/docs/source/reference/modding/settings/GUI.rst index 763e64ab99..8a27ed2c01 100644 --- a/docs/source/reference/modding/settings/GUI.rst +++ b/docs/source/reference/modding/settings/GUI.rst @@ -157,8 +157,8 @@ color topic specific -------------------- :Type: RGBA floating point -:Range: 0.0 to 1.0 -:Default: empty +:Range: 0.0 to 1.0 for each channel +:Default: 0.45 0.5 0.8 1 (blue) This setting overrides the colour of dialogue topics that have a response unique to the actors speaking. The value is composed of four floating point values representing the red, green, blue and alpha channels. @@ -166,15 +166,67 @@ The alpha value is currently ignored. A topic response is considered unique if its Actor filter field contains the speaking actor's object ID and hasn't yet been read. +color topic specific over +------------------------- + +:Type: RGBA floating point +:Range: 0.0 to 1.0 for each channel +:Default: 0.6 0.6 0.85 1 (blue) + +This setting provides an "over" colour to dialogue topics that meet the color topic specific criteria. +The value is composed of four floating point values representing the red, green, blue and alpha channels. +The alpha value is currently ignored. + +A dialogue topic is considered "over" if it is the active GUI element through keyboard or mouse events. + +color topic specific pressed +---------------------------- + +:Type: RGBA floating point +:Range: 0.0 to 1.0 for each channel +:Default: 0.3 0.35 0.75 1 (blue) + +This setting provides an "pressed" colour to dialogue topics that meet the color topic specific criteria. +The value is composed of four floating point values representing the red, green, blue and alpha channels. +The alpha value is currently ignored. + +A dialogue topic is considered "pressed" if it is the active GUI element and it receives a sustained keyboard or mouse event. + color topic exhausted --------------------- :Type: RGBA floating point -:Range: 0.0 to 1.0 -:Default: empty +:Range: 0.0 to 1.0 for each channel +:Default: 0.3 0.3 0.3 1 (grey) This setting overrides the colour of dialogue topics which have been "exhausted" by the player. The value is composed of four floating point values representing the red, green, blue and alpha channels. The alpha value is currently ignored. A topic is considered "exhausted" if the response the player is about to see has already been seen. + +color topic exhausted over +-------------------------- + +:Type: RGBA floating point +:Range: 0.0 to 1.0 for each channel +:Default: 0.55 0.55 0.55 1 (grey) + +This setting provides an "over" colour to dialogue topics that meet the color topic exhausted criteria. +The value is composed of four floating point values representing the red, green, blue and alpha channels. +The alpha value is currently ignored. + +A dialogue topic is considered "over" if it is the active GUI element through keyboard or mouse events. + +color topic exhausted pressed +----------------------------- + +:Type: RGBA floating point +:Range: 0.0 to 1.0 for each channel +:Default: 0.45 0.45 0.45 1 (grey) + +This setting provides a "pressed" colour to dialogue topics that meet the color topic exhausted criteria. +The value is composed of four floating point values representing the red, green, blue and alpha channels. +The alpha value is currently ignored. + +A dialogue topic is considered "pressed" if it is the active GUI element and it receives a sustained keyboard or mouse event. diff --git a/files/data/l10n/OMWCamera/fr.yaml b/files/data/l10n/OMWCamera/fr.yaml index a9dde33f79..00a0e3d3c1 100644 --- a/files/data/l10n/OMWCamera/fr.yaml +++ b/files/data/l10n/OMWCamera/fr.yaml @@ -1,5 +1,5 @@ -Camera: "Caméra d’OpenMW" -settingsPageDescription: "Configuration de la caméra d’OpenMW" +Camera: "OpenMW : Caméra" +settingsPageDescription: "Paramètres de la caméra d’OpenMW" thirdPersonSettings: "Vue à la troisième personne" diff --git a/files/data/l10n/OMWControls/fr.yaml b/files/data/l10n/OMWControls/fr.yaml index 95fa88a6ac..7dc18dc00f 100644 --- a/files/data/l10n/OMWControls/fr.yaml +++ b/files/data/l10n/OMWControls/fr.yaml @@ -1,19 +1,19 @@ ControlsPage: "OpenMW : Contrôles" -ControlsPageDescription: "Paramètres additionnels de contrôle" +ControlsPageDescription: "Paramètres additionnels des contrôles d'OpenMW" MovementSettings: "Mouvements" alwaysRun: "Course permanente" alwaysRunDescription: | - Actif : Le personnage se déplace par défaut en courant ; \n\n - Inactif : Le personnage se déplace par défaut en marchant.\n\n - La touche Maj. inverse temporairement ce paramètre.\n\n + Actif : Le personnage se déplace par défaut en courant ; + Inactif : Le personnage se déplace par défaut en marchant. + La touche Maj. inverse temporairement ce paramètre. La touche Verr Maj inverse ce paramètre lorsqu'elle est verrouillée. toggleSneak: "Mode discrétion maintenu" toggleSneakDescription: | - Une simple pression de la touche associée (Ctrl par défaut) active le mode discrétion, une seconde pression désactive le mode discrétion.\n\n - Il n'est plus nécessaire de maintenir une touche appuyée pour que le mode discrétion soit actif.\n\n + Une simple pression de la touche associée (Ctrl par défaut) active le mode discrétion, une seconde pression désactive le mode discrétion. + Il n'est plus nécessaire de maintenir une touche appuyée pour que le mode discrétion soit actif. Certains joueurs ayant une utilisation intensive du mode discrétion considèrent qu'il est plus aisé de contrôler leur personnage ainsi. smoothControllerMovement: "Mouvements à la manette adoucis" diff --git a/files/data/l10n/OMWMusic/fr.yaml b/files/data/l10n/OMWMusic/fr.yaml index 0e15d0a411..0f2977dbf0 100755 --- a/files/data/l10n/OMWMusic/fr.yaml +++ b/files/data/l10n/OMWMusic/fr.yaml @@ -1,7 +1,7 @@ -#Music: "OpenMW Music" -#settingsPageDescription: "OpenMW Music settings" +Music: "OpenMW : Musique" +settingsPageDescription: "Paramètre de la musique d'OpenMW" -#musicSettings: "Music configuration" +musicSettings: "Configuration de la musique" -#CombatMusicEnabled: "Play combat music" -#CombatMusicEnabledDescription: "Whether to switch to combat music when there are actors in combat." +CombatMusicEnabled: "Jouer la musique de combat" +CombatMusicEnabledDescription: "Si activé, le jeu bascule vers la musique de combat dès qu'un personnage est en combat." diff --git a/files/data/mygui/openmw_list.skin.xml b/files/data/mygui/openmw_list.skin.xml index 75f4b9e850..d17b258b80 100644 --- a/files/data/mygui/openmw_list.skin.xml +++ b/files/data/mygui/openmw_list.skin.xml @@ -124,6 +124,28 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/data/scripts/omw/music/actor.lua b/files/data/scripts/omw/music/actor.lua index 4890387e54..8f3ac7915a 100755 --- a/files/data/scripts/omw/music/actor.lua +++ b/files/data/scripts/omw/music/actor.lua @@ -24,7 +24,7 @@ local function onUpdate() -- Early-out for actors without targets and without combat state -- TODO: use events or engine handlers to detect when targets change local isStanceNothing = types.Actor.getStance(self) == types.Actor.STANCE.Nothing - if isStanceNothing and next(targets) == nil then + if isStanceNothing and next(targets) == nil and not AI.isFleeing() then return end diff --git a/files/lua_api/openmw/ambient.lua b/files/lua_api/openmw/ambient.lua index ff776f84fb..82d4dfdeee 100644 --- a/files/lua_api/openmw/ambient.lua +++ b/files/lua_api/openmw/ambient.lua @@ -1,6 +1,6 @@ --- --- `openmw.ambient` controls background sounds, specific to given player (2D-sounds). --- Can be used only by menu scripts and local scripts, that are attached to a player. +-- `openmw.ambient` controls background 2D sounds specific to a given player. +-- Can be used only by menu scripts and local scripts that are attached to a player. -- @module ambient -- @usage local ambient = require('openmw.ambient') @@ -12,11 +12,11 @@ -- @param #string soundId ID of Sound record to play -- @param #table options An optional table with additional optional arguments. Can contain: -- --- * `timeOffset` - a floating point number >= 0, to skip some time (in seconds) from beginning of sound file (default: 0); --- * `volume` - a floating point number >= 0, to set a sound volume (default: 1); --- * `pitch` - a floating point number >= 0, to set a sound pitch (default: 1); --- * `scale` - a boolean, to set if sound pitch should be scaled by simulation time scaling (default: true); --- * `loop` - a boolean, to set if sound should be repeated when it ends (default: false); +-- * `timeOffset` - a floating point number >= 0, to skip some time (in seconds) from the beginning of the sound (default: 0); +-- * `volume` - a floating point number >= 0, to set the sound's volume (default: 1); +-- * `pitch` - a floating point number >= 0, to set the sound's pitch (default: 1); +-- * `scale` - a boolean, to set if the sound's pitch should be scaled by simulation time scaling (default: true); +-- * `loop` - a boolean, to set if the sound should be repeated when it ends (default: false); -- @usage local params = { -- timeOffset=0.1 -- volume=0.3, @@ -29,14 +29,14 @@ --- -- Play a 2D sound file -- @function [parent=#ambient] playSoundFile --- @param #string fileName Path to sound file in VFS +-- @param #string fileName Path to a sound file in VFS -- @param #table options An optional table with additional optional arguments. Can contain: -- --- * `timeOffset` - a floating point number >= 0, to skip some time (in seconds) from beginning of sound file (default: 0); --- * `volume` - a floating point number >= 0, to set a sound volume (default: 1); --- * `pitch` - a floating point number >= 0, to set a sound pitch (default: 1); --- * `scale` - a boolean, to set if sound pitch should be scaled by simulation time scaling (default: true); --- * `loop` - a boolean, to set if sound should be repeated when it ends (default: false); +-- * `timeOffset` - a floating point number >= 0, to skip some time (in seconds) from the beginning of the sound file (default: 0); +-- * `volume` - a floating point number >= 0, to set the sound's volume (default: 1); +-- * `pitch` - a floating point number >= 0, to set the sound's pitch (default: 1); +-- * `scale` - a boolean, to set if the sound's pitch should be scaled by simulation time scaling (default: true); +-- * `loop` - a boolean, to set if the sound should be repeated when it ends (default: false); -- @usage local params = { -- timeOffset=0.1 -- volume=0.3, @@ -55,37 +55,37 @@ --- -- Stop a sound file -- @function [parent=#ambient] stopSoundFile --- @param #string fileName Path to sound file in VFS +-- @param #string fileName Path to a sound file in VFS -- @usage ambient.stopSoundFile("Sound\\test.mp3"); --- --- Check if sound is playing +-- Check if a sound is playing -- @function [parent=#ambient] isSoundPlaying -- @param #string soundId ID of Sound record to check -- @return #boolean -- @usage local isPlaying = ambient.isSoundPlaying("shock bolt"); --- --- Check if sound file is playing +-- Check if a sound file is playing -- @function [parent=#ambient] isSoundFilePlaying --- @param #string fileName Path to sound file in VFS +-- @param #string fileName Path to a sound file in VFS -- @return #boolean -- @usage local isPlaying = ambient.isSoundFilePlaying("Sound\\test.mp3"); --- -- Play a sound file as a music track -- @function [parent=#ambient] streamMusic --- @param #string fileName Path to file in VFS +-- @param #string fileName Path to a file in VFS -- @param #table options An optional table with additional optional arguments. Can contain: -- --- * `fadeOut` - a floating point number >= 0, time (in seconds) to fade out current track before playing this one (default 1.0); +-- * `fadeOut` - a floating point number >= 0, time (in seconds) to fade out the current track before playing this one (default 1.0); -- @usage local params = { -- fadeOut=2.0 -- }; -- ambient.streamMusic("Music\\Test\\Test.mp3", params) --- --- Stop to play current music +-- Stop the currently playing music -- @function [parent=#ambient] stopMusic -- @usage ambient.stopMusic(); @@ -98,7 +98,7 @@ --- -- Play an ambient voiceover. -- @function [parent=#ambient] say --- @param #string fileName Path to sound file in VFS +-- @param #string fileName Path to a sound file in VFS -- @param #string text Subtitle text (optional) -- @usage -- play voiceover and print messagebox -- ambient.say("Sound\\Vo\\Misc\\voice.mp3", "Subtitle text") @@ -108,7 +108,7 @@ --- -- Stop an ambient voiceover -- @function [parent=#ambient] stopSay --- @param #string fileName Path to sound file in VFS +-- @param #string fileName Path to a sound file in VFS -- @usage ambient.stopSay(); --- diff --git a/files/lua_api/openmw/animation.lua b/files/lua_api/openmw/animation.lua index ba57708a94..065a150605 100644 --- a/files/lua_api/openmw/animation.lua +++ b/files/lua_api/openmw/animation.lua @@ -1,5 +1,5 @@ --- --- `openmw.animation` defines functions that allow control of character animations +-- `openmw.animation` defines functions that allow control of character animations. -- Note that for some methods, such as @{openmw.animation#playBlended} you should use the associated methods on the -- [AnimationController](interface_animation.html) interface rather than invoking this API directly. -- @module animation @@ -55,7 +55,7 @@ -- @return #boolean --- --- Skips animations for one frame, equivalent to mwscript's SkipAnim +-- Skips animations for one frame, equivalent to mwscript's SkipAnim. -- Can be used only in local scripts on self. -- @function [parent=#animation] skipAnimationThisFrame -- @param openmw.core#GameObject actor @@ -98,7 +98,7 @@ --- --- Cancels and removes the animation group from the list of active animations +-- Cancels and removes the animation group from the list of active animations. -- Can be used only in local scripts on self. -- @function [parent=#animation] cancel -- @param openmw.core#GameObject actor diff --git a/files/lua_api/openmw/async.lua b/files/lua_api/openmw/async.lua index 64080ed96a..7a410336c0 100644 --- a/files/lua_api/openmw/async.lua +++ b/files/lua_api/openmw/async.lua @@ -1,5 +1,5 @@ --- --- `openmw.async` contains timers and coroutine utils. All functions require +-- `openmw.async` contains timers and coroutine utilities. All functions require -- the package itself as a first argument. -- @module async -- @usage local async = require('openmw.async') @@ -16,7 +16,7 @@ --- -- Calls callback(arg) in `delay` simulation seconds. --- Callback must be registered in advance. +-- The callback must be registered in advance. -- @function [parent=#async] newSimulationTimer -- @param self -- @param #number delay @@ -25,7 +25,7 @@ --- -- Calls callback(arg) in `delay` game seconds. --- Callback must be registered in advance. +-- The callback must be registered in advance. -- @function [parent=#async] newGameTimer -- @param self -- @param #number delay @@ -49,7 +49,7 @@ -- @param #function func --- --- Wraps Lua function with `Callback` object that can be used in async API calls. +-- Wraps a Lua function with a `Callback` object that can be used in async API calls. -- @function [parent=#async] callback -- @param self -- @param #function func diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 1aab8554ae..e4636f0ce9 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -218,14 +218,14 @@ keyboard navigation = true color topic enable = false # The color of dialogue topic keywords that gives unique actor responses -# Format R G B A or empty for no special formatting -# Default to blue color topic specific = 0.45 0.5 0.8 1 +color topic specific over = 0.6 0.6 0.85 1 +color topic specific pressed = 0.3 0.35 0.75 1 # The color of dialogue topic keywords that gives already read responses -# Format R G B A or empty for no special formatting -# Default to grey color topic exhausted = 0.3 0.3 0.3 1 +color topic exhausted over = 0.55 0.55 0.55 1 +color topic exhausted pressed = 0.45 0.45 0.45 1 [HUD]