From 2dbe30ed5c790b0aed862a3dd90dca1e3d076c80 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Thu, 23 May 2024 17:15:56 +0200 Subject: [PATCH 01/23] Update effects upon applying them --- apps/openmw/mwmechanics/activespells.cpp | 258 +++++++++++++---------- apps/openmw/mwmechanics/activespells.hpp | 8 +- apps/openmw/mwmechanics/spellcasting.cpp | 4 - apps/openmw/mwmechanics/spelleffects.cpp | 6 +- 4 files changed, 156 insertions(+), 120 deletions(-) diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index 270faf8598..f9b7ec57ea 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -73,6 +73,20 @@ namespace namespace MWMechanics { + struct ActiveSpells::UpdateContext + { + bool mUpdatedEnemy = false; + bool mUpdatedHitOverlay = false; + bool mUpdateSpellWindow = false; + bool mPlayNonLooping = false; + bool mUpdate; + + UpdateContext(bool update) + : mUpdate(update) + { + } + }; + ActiveSpells::IterationGuard::IterationGuard(ActiveSpells& spells) : mActiveSpells(spells) { @@ -256,8 +270,9 @@ namespace MWMechanics ++spellIt; } + UpdateContext context(duration > 0.f); for (const auto& spell : mQueue) - addToSpells(ptr, spell); + addToSpells(ptr, spell, context); mQueue.clear(); // Vanilla only does this on cell change I think @@ -267,20 +282,17 @@ namespace MWMechanics if (spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power && !isSpellActive(spell->mId)) { - mSpells.emplace_back(ActiveSpellParams{ spell, ptr, true }); - mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); + initParams(ptr, ActiveSpellParams{ spell, ptr, true }, context); } } - bool updateSpellWindow = false; - bool playNonLooping = false; if (ptr.getClass().hasInventoryStore(ptr) && !(creatureStats.isDead() && !creatureStats.isDeathAnimationFinished())) { auto& store = ptr.getClass().getInventoryStore(ptr); if (store.getInvListener() != nullptr) { - playNonLooping = !store.isFirstEquip(); + context.mPlayNonLooping = !store.isFirstEquip(); const auto world = MWBase::Environment::get().getWorld(); for (int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++) { @@ -306,117 +318,18 @@ namespace MWMechanics // invisibility manually purgeEffect(ptr, ESM::MagicEffect::Invisibility); applyPurges(ptr); - ActiveSpellParams& params = mSpells.emplace_back(ActiveSpellParams{ *slot, enchantment, ptr }); - params.setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); - updateSpellWindow = true; + ActiveSpellParams* params = initParams(ptr, ActiveSpellParams{ *slot, enchantment, ptr }, context); + if (params) + context.mUpdateSpellWindow = true; } } } const MWWorld::Ptr player = MWMechanics::getPlayer(); - bool updatedHitOverlay = false; - bool updatedEnemy = false; // Update effects for (auto spellIt = mSpells.begin(); spellIt != mSpells.end();) { - const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId( - spellIt->mCasterActorId); // Maybe make this search outside active grid? - bool removedSpell = false; - std::optional reflected; - for (auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();) - { - auto result = applyMagicEffect(ptr, caster, *spellIt, *it, duration, playNonLooping); - if (result.mType == MagicApplicationResult::Type::REFLECTED) - { - if (!reflected) - { - if (Settings::game().mClassicReflectedAbsorbSpellsBehavior) - reflected = { *spellIt, caster }; - else - reflected = { *spellIt, ptr }; - } - auto& reflectedEffect = reflected->mEffects.emplace_back(*it); - reflectedEffect.mFlags - = ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; - it = spellIt->mEffects.erase(it); - } - else if (result.mType == MagicApplicationResult::Type::REMOVED) - it = spellIt->mEffects.erase(it); - else - { - ++it; - if (!updatedEnemy && result.mShowHealth && caster == player && ptr != player) - { - MWBase::Environment::get().getWindowManager()->setEnemy(ptr); - updatedEnemy = true; - } - if (!updatedHitOverlay && result.mShowHit && ptr == player) - { - MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); - updatedHitOverlay = true; - } - } - removedSpell = applyPurges(ptr, &spellIt, &it); - if (removedSpell) - break; - } - if (reflected) - { - const ESM::Static* reflectStatic = MWBase::Environment::get().getESMStore()->get().find( - ESM::RefId::stringRefId("VFX_Reflect")); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); - if (animation && !reflectStatic->mModel.empty()) - { - const VFS::Path::Normalized reflectStaticModel - = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(reflectStatic->mModel)); - animation->addEffect( - reflectStaticModel, ESM::MagicEffect::indexToName(ESM::MagicEffect::Reflect), false); - } - caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected); - } - if (removedSpell) - continue; - - bool remove = false; - if (spellIt->hasFlag(ESM::ActiveSpells::Flag_SpellStore)) - { - try - { - remove = !spells.hasSpell(spellIt->mSourceSpellId); - } - catch (const std::runtime_error& e) - { - remove = true; - Log(Debug::Error) << "Removing active effect: " << e.what(); - } - } - else if (spellIt->hasFlag(ESM::ActiveSpells::Flag_Equipment)) - { - // Remove effects tied to equipment that has been unequipped - const auto& store = ptr.getClass().getInventoryStore(ptr); - remove = true; - for (int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++) - { - auto slot = store.getSlot(slotIndex); - if (slot != store.end() && slot->getCellRef().getRefNum().isSet() - && slot->getCellRef().getRefNum() == spellIt->mItem) - { - remove = false; - break; - } - } - } - if (remove) - { - auto params = *spellIt; - spellIt = mSpells.erase(spellIt); - for (const auto& effect : params.mEffects) - onMagicEffectRemoved(ptr, params, effect); - applyPurges(ptr, &spellIt); - updateSpellWindow = true; - continue; - } - ++spellIt; + updateActiveSpell(ptr, duration, spellIt, context); } if (Settings::game().mClassicCalmSpellsBehavior) @@ -427,7 +340,7 @@ namespace MWMechanics creatureStats.getAiSequence().stopCombat(); } - if (ptr == player && updateSpellWindow) + if (ptr == player && context.mUpdateSpellWindow) { // Something happened with the spell list -- possibly while the game is paused, // so we want to make the spell window get the memo. @@ -436,7 +349,125 @@ namespace MWMechanics } } - void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell) + bool ActiveSpells::updateActiveSpell( + const MWWorld::Ptr& ptr, float duration, Collection::iterator& spellIt, UpdateContext& context) + { + const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId( + spellIt->mCasterActorId); // Maybe make this search outside active grid? + bool removedSpell = false; + std::optional reflected; + for (auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();) + { + auto result = applyMagicEffect(ptr, caster, *spellIt, *it, duration, context.mPlayNonLooping); + if (result.mType == MagicApplicationResult::Type::REFLECTED) + { + if (!reflected) + { + if (Settings::game().mClassicReflectedAbsorbSpellsBehavior) + reflected = { *spellIt, caster }; + else + reflected = { *spellIt, ptr }; + } + auto& reflectedEffect = reflected->mEffects.emplace_back(*it); + reflectedEffect.mFlags + = ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; + it = spellIt->mEffects.erase(it); + } + else if (result.mType == MagicApplicationResult::Type::REMOVED) + it = spellIt->mEffects.erase(it); + else + { + const MWWorld::Ptr player = MWMechanics::getPlayer(); + ++it; + if (!context.mUpdatedEnemy && result.mShowHealth && caster == player && ptr != player) + { + MWBase::Environment::get().getWindowManager()->setEnemy(ptr); + context.mUpdatedEnemy = true; + } + if (!context.mUpdatedHitOverlay && result.mShowHit && ptr == player) + { + MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); + context.mUpdatedHitOverlay = true; + } + } + removedSpell = applyPurges(ptr, &spellIt, &it); + if (removedSpell) + break; + } + if (reflected) + { + const ESM::Static* reflectStatic = MWBase::Environment::get().getESMStore()->get().find( + ESM::RefId::stringRefId("VFX_Reflect")); + MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); + if (animation && !reflectStatic->mModel.empty()) + { + const VFS::Path::Normalized reflectStaticModel + = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(reflectStatic->mModel)); + animation->addEffect( + reflectStaticModel, ESM::MagicEffect::indexToName(ESM::MagicEffect::Reflect), false); + } + caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected); + } + if (removedSpell) + return true; + + bool remove = false; + if (spellIt->hasFlag(ESM::ActiveSpells::Flag_SpellStore)) + { + try + { + auto& spells = ptr.getClass().getCreatureStats(ptr).getSpells(); + remove = !spells.hasSpell(spellIt->mSourceSpellId); + } + catch (const std::runtime_error& e) + { + remove = true; + Log(Debug::Error) << "Removing active effect: " << e.what(); + } + } + else if (spellIt->hasFlag(ESM::ActiveSpells::Flag_Equipment)) + { + // Remove effects tied to equipment that has been unequipped + const auto& store = ptr.getClass().getInventoryStore(ptr); + remove = true; + for (int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++) + { + auto slot = store.getSlot(slotIndex); + if (slot != store.end() && slot->getCellRef().getRefNum().isSet() + && slot->getCellRef().getRefNum() == spellIt->mItem) + { + remove = false; + break; + } + } + } + if (remove) + { + auto params = *spellIt; + spellIt = mSpells.erase(spellIt); + for (const auto& effect : params.mEffects) + onMagicEffectRemoved(ptr, params, effect); + applyPurges(ptr, &spellIt); + context.mUpdateSpellWindow = true; + return true; + } + ++spellIt; + return false; + } + + ActiveSpells::ActiveSpellParams* ActiveSpells::initParams( + const MWWorld::Ptr& ptr, const ActiveSpellParams& params, UpdateContext& context) + { + mSpells.emplace_back(params).setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); + auto it = mSpells.end(); + --it; + // We instantly apply the effect with a duration of 0 so continuous effects can be purged before truly applying + if (context.mUpdate && updateActiveSpell(ptr, 0.f, it, context)) + return nullptr; + return &*it; + } + + void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell, UpdateContext& context) { if (!spell.hasFlag(ESM::ActiveSpells::Flag_Stackable)) { @@ -454,8 +485,7 @@ namespace MWMechanics onMagicEffectRemoved(ptr, params, effect); } } - mSpells.emplace_back(spell); - mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); + initParams(ptr, spell, context); } ActiveSpells::ActiveSpells() @@ -608,6 +638,8 @@ namespace MWMechanics { purge( [=](const ActiveSpellParams&, const ESM::ActiveEffect& effect) { + if (!(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) + return false; if (effectArg.empty()) return effect.mEffectId == effectId; return effect.mEffectId == effectId && effect.getSkillOrAttribute() == effectArg; diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index 3e4dafdb26..465e5aa456 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -116,17 +116,23 @@ namespace MWMechanics IterationGuard(ActiveSpells& spells); ~IterationGuard(); }; + struct UpdateContext; std::list mSpells; std::vector mQueue; std::queue mPurges; bool mIterating; - void addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell); + void addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell, UpdateContext& context); bool applyPurges(const MWWorld::Ptr& ptr, std::list::iterator* currentSpell = nullptr, std::vector::iterator* currentEffect = nullptr); + bool updateActiveSpell( + const MWWorld::Ptr& ptr, float duration, Collection::iterator& spellIt, UpdateContext& context); + + ActiveSpellParams* initParams(const MWWorld::Ptr& ptr, const ActiveSpellParams& params, UpdateContext& context); + public: ActiveSpells(); diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 59e7e29a38..bf9d6aa025 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -215,10 +215,6 @@ namespace MWMechanics bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); effect.mDuration = hasDuration ? static_cast(enam.mData.mDuration) : 1.f; - bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce; - if (!appliedOnce) - effect.mDuration = std::max(1.f, effect.mDuration); - effect.mTimeLeft = effect.mDuration; // add to list of active effects, to apply in next frame diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index 99e5a09481..822c394352 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -1011,11 +1011,13 @@ namespace MWMechanics else { // Morrowind.exe doesn't apply magic effects while the menu is open, we do because we like to see stats - // updated instantly. We don't want to teleport instantly though + // updated instantly. We don't want to teleport instantly though. Nor do we want to force players to drink + // invisibility potions in the "right" order if (!dt && (effect.mEffectId == ESM::MagicEffect::Recall || effect.mEffectId == ESM::MagicEffect::DivineIntervention - || effect.mEffectId == ESM::MagicEffect::AlmsiviIntervention)) + || effect.mEffectId == ESM::MagicEffect::AlmsiviIntervention + || effect.mEffectId == ESM::MagicEffect::Invisibility)) return { MagicApplicationResult::Type::APPLIED, receivedMagicDamage, affectedHealth }; auto& stats = target.getClass().getCreatureStats(target); auto& magnitudes = stats.getMagicEffects(); From c80b3d26b9cb0b8198b6d12364e0fb632b37019c Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sun, 13 Jul 2025 11:18:15 +0200 Subject: [PATCH 02/23] Only check for spelllist/equipment changes in the update loop --- apps/openmw/mwmechanics/activespells.cpp | 73 +++++++++++++----------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index f9b7ec57ea..7a4369a464 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -79,6 +79,7 @@ namespace MWMechanics bool mUpdatedHitOverlay = false; bool mUpdateSpellWindow = false; bool mPlayNonLooping = false; + bool mEraseRemoved = false; bool mUpdate; UpdateContext(bool update) @@ -327,6 +328,7 @@ namespace MWMechanics const MWWorld::Ptr player = MWMechanics::getPlayer(); // Update effects + context.mEraseRemoved = true; for (auto spellIt = mSpells.begin(); spellIt != mSpells.end();) { updateActiveSpell(ptr, duration, spellIt, context); @@ -411,45 +413,48 @@ namespace MWMechanics if (removedSpell) return true; - bool remove = false; - if (spellIt->hasFlag(ESM::ActiveSpells::Flag_SpellStore)) + if (context.mEraseRemoved) { - try + bool remove = false; + if (spellIt->hasFlag(ESM::ActiveSpells::Flag_SpellStore)) { - auto& spells = ptr.getClass().getCreatureStats(ptr).getSpells(); - remove = !spells.hasSpell(spellIt->mSourceSpellId); - } - catch (const std::runtime_error& e) - { - remove = true; - Log(Debug::Error) << "Removing active effect: " << e.what(); - } - } - else if (spellIt->hasFlag(ESM::ActiveSpells::Flag_Equipment)) - { - // Remove effects tied to equipment that has been unequipped - const auto& store = ptr.getClass().getInventoryStore(ptr); - remove = true; - for (int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++) - { - auto slot = store.getSlot(slotIndex); - if (slot != store.end() && slot->getCellRef().getRefNum().isSet() - && slot->getCellRef().getRefNum() == spellIt->mItem) + try { - remove = false; - break; + auto& spells = ptr.getClass().getCreatureStats(ptr).getSpells(); + remove = !spells.hasSpell(spellIt->mSourceSpellId); + } + catch (const std::runtime_error& e) + { + remove = true; + Log(Debug::Error) << "Removing active effect: " << e.what(); } } - } - if (remove) - { - auto params = *spellIt; - spellIt = mSpells.erase(spellIt); - for (const auto& effect : params.mEffects) - onMagicEffectRemoved(ptr, params, effect); - applyPurges(ptr, &spellIt); - context.mUpdateSpellWindow = true; - return true; + else if (spellIt->hasFlag(ESM::ActiveSpells::Flag_Equipment)) + { + // Remove effects tied to equipment that has been unequipped + const auto& store = ptr.getClass().getInventoryStore(ptr); + remove = true; + for (int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++) + { + auto slot = store.getSlot(slotIndex); + if (slot != store.end() && slot->getCellRef().getRefNum().isSet() + && slot->getCellRef().getRefNum() == spellIt->mItem) + { + remove = false; + break; + } + } + } + if (remove) + { + auto params = *spellIt; + spellIt = mSpells.erase(spellIt); + for (const auto& effect : params.mEffects) + onMagicEffectRemoved(ptr, params, effect); + applyPurges(ptr, &spellIt); + context.mUpdateSpellWindow = true; + return true; + } } ++spellIt; return false; From d899454f3618b85608cba374e5f50373c4fd10ff Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Fri, 11 Apr 2025 06:08:42 +0300 Subject: [PATCH 03/23] Remove completion threshold-based turning for the player (#8447) --- apps/openmw/mwmechanics/character.cpp | 48 ++++++++------------------- 1 file changed, 14 insertions(+), 34 deletions(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 1aac063ce3..3f4f6c6956 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -2310,17 +2310,13 @@ namespace MWMechanics } else { - // Do not play turning animation for player if rotation speed is very slow. - // Actual threshold should take framerate in account. - float rotationThreshold = (isPlayer ? 0.015f : 0.001f) * 60 * duration; - // It seems only bipedal actors use turning animations. // Also do not use turning animations in the first-person view and when sneaking. if (!sneak && !isFirstPersonPlayer && isBiped) { - if (effectiveRotation > rotationThreshold) + if (effectiveRotation > 0.f) movestate = inwater ? CharState_SwimTurnRight : CharState_TurnRight; - else if (effectiveRotation < -rotationThreshold) + else if (effectiveRotation < 0.f) movestate = inwater ? CharState_SwimTurnLeft : CharState_TurnLeft; } } @@ -2346,34 +2342,19 @@ namespace MWMechanics vec.y() *= std::sqrt(1.0f - swimUpwardCoef * swimUpwardCoef); } - // Player can not use smooth turning as NPCs, so we play turning animation a bit to avoid jittering - if (isPlayer) + if (isBiped) { - float threshold = mCurrentMovement.find("swim") == std::string::npos ? 0.4f : 0.8f; - float complete; - bool animPlaying = mAnimation->getInfo(mCurrentMovement, &complete); - if (movestate == CharState_None && jumpstate == JumpState_None && isTurning()) - { - if (animPlaying && complete < threshold) - movestate = mMovementState; - } - } - else - { - if (isBiped) - { - if (mTurnAnimationThreshold > 0) - mTurnAnimationThreshold -= duration; + if (mTurnAnimationThreshold > 0) + mTurnAnimationThreshold -= duration; - if (movestate == CharState_TurnRight || movestate == CharState_TurnLeft - || movestate == CharState_SwimTurnRight || movestate == CharState_SwimTurnLeft) - { - mTurnAnimationThreshold = 0.05f; - } - else if (movestate == CharState_None && isTurning() && mTurnAnimationThreshold > 0) - { - movestate = mMovementState; - } + if (movestate == CharState_TurnRight || movestate == CharState_TurnLeft + || movestate == CharState_SwimTurnRight || movestate == CharState_SwimTurnLeft) + { + mTurnAnimationThreshold = 0.05f; + } + else if (movestate == CharState_None && isTurning() && mTurnAnimationThreshold > 0) + { + movestate = mMovementState; } } @@ -2402,11 +2383,10 @@ namespace MWMechanics if (isTurning()) { - // Adjust animation speed from 1.0 to 1.5 multiplier if (duration > 0) { float turnSpeed = std::min(1.5f, std::abs(rot.z()) / duration / static_cast(osg::PI)); - mAnimation->adjustSpeedMult(mCurrentMovement, std::max(turnSpeed, 1.0f)); + mAnimation->adjustSpeedMult(mCurrentMovement, turnSpeed); } } else if (mMovementState != CharState_None && mAdjustMovementAnimSpeed) From 896d6fd01ef1b305f76e1e1fdbad7716b112c332 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sat, 22 Mar 2025 21:11:33 +0300 Subject: [PATCH 04/23] Put combat actions on hold when the actor is incapacitated (#7979) --- apps/openmw/mwmechanics/aicombat.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index a6f9935194..7b7148f1de 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -178,11 +178,16 @@ namespace MWMechanics currentCell = actor.getCell(); } + const MWWorld::Class& actorClass = actor.getClass(); + MWMechanics::CreatureStats& stats = actorClass.getCreatureStats(actor); + if (stats.isParalyzed() || stats.getKnockedDown()) + return false; + bool forceFlee = false; if (!canFight(actor, target)) { storage.stopAttack(); - actor.getClass().getCreatureStats(actor).setAttackingOrSpell(false); + stats.setAttackingOrSpell(false); storage.mActionCooldown = 0.f; // Continue combat if target is player or player follower/escorter and an attack has been attempted const auto& playerFollowersAndEscorters @@ -191,18 +196,14 @@ namespace MWMechanics = (std::find(playerFollowersAndEscorters.begin(), playerFollowersAndEscorters.end(), target) != playerFollowersAndEscorters.end()); if ((target == MWMechanics::getPlayer() || targetSidesWithPlayer) - && ((actor.getClass().getCreatureStats(actor).getHitAttemptActorId() - == target.getClass().getCreatureStats(target).getActorId()) - || (target.getClass().getCreatureStats(target).getHitAttemptActorId() - == actor.getClass().getCreatureStats(actor).getActorId()))) + && ((stats.getHitAttemptActorId() == target.getClass().getCreatureStats(target).getActorId()) + || (target.getClass().getCreatureStats(target).getHitAttemptActorId() == stats.getActorId()))) forceFlee = true; else // Otherwise end combat return true; } - const MWWorld::Class& actorClass = actor.getClass(); - actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); - + stats.setMovementFlag(CreatureStats::Flag_Run, true); float& actionCooldown = storage.mActionCooldown; std::unique_ptr& currentAction = storage.mCurrentAction; @@ -330,7 +331,7 @@ namespace MWMechanics { storage.mUseCustomDestination = false; storage.stopAttack(); - actor.getClass().getCreatureStats(actor).setAttackingOrSpell(false); + stats.setAttackingOrSpell(false); currentAction = std::make_unique(); actionCooldown = currentAction->getActionCooldown(); storage.startFleeing(); From 5dddf9153d72d681c5e76cbe3b2f447104c892c2 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Thu, 24 Jul 2025 00:25:26 +0300 Subject: [PATCH 05/23] Fix useAmbientLight references in docs --- files/lua_api/openmw/animation.lua | 2 +- files/lua_api/openmw/world.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/files/lua_api/openmw/animation.lua b/files/lua_api/openmw/animation.lua index 847ba9bd78..929a784ba9 100644 --- a/files/lua_api/openmw/animation.lua +++ b/files/lua_api/openmw/animation.lua @@ -229,7 +229,7 @@ -- * `boneName` - name of the bone to attach the vfx to. (default: "") -- * `particleTextureOverride` - name of the particle texture to use. (default: "") -- * `vfxId` - a string ID that can be used to remove the effect later, using #removeVfx, and to avoid duplicate effects. The default value of "" can have duplicates. To avoid interaction with the engine, use unique identifiers unrelated to magic effect IDs. The engine uses this identifier to add and remove magic effects based on what effects are active on the actor. If this is set equal to the @{openmw.core#MagicEffectId} identifier of the magic effect being added, for example core.magic.EFFECT_TYPE.FireDamage, then the engine will remove it once the fire damage effect on the actor reaches 0. (Default: ""). --- * `useAmbientLighting` - boolean, vfx get a white ambient light attached in Morrowind. If false don't attach this. (default: true) +-- * `useAmbientLight` - boolean, vfx get a white ambient light attached in Morrowind. If false don't attach this. (default: true) -- -- @usage local mgef = core.magic.effects.records[myEffectName] -- anim.addVfx(self, 'VFX_Hands', {boneName = 'Bip01 L Hand', particleTextureOverride = mgef.particle, loop = mgef.continuousVfx, vfxId = mgef.id..'_myuniquenamehere'}) diff --git a/files/lua_api/openmw/world.lua b/files/lua_api/openmw/world.lua index c459fafca4..ae494f8c99 100644 --- a/files/lua_api/openmw/world.lua +++ b/files/lua_api/openmw/world.lua @@ -195,7 +195,7 @@ -- * `mwMagicVfx` - Boolean that if true causes the textureOverride parameter to only affect nodes with the Nif::RC_NiTexturingProperty property set. (default: true). -- * `particleTextureOverride` - Name of a particle texture that should override this effect's default texture. (default: "") -- * `scale` - A number that scales the size of the vfx (Default: 1) --- * `useAmbientLighting` - boolean, vfx get a white ambient light attached in Morrowind. If false don't attach this. (default: 1) +-- * `useAmbientLight` - boolean, vfx get a white ambient light attached in Morrowind. If false don't attach this. (default: true) -- -- @usage -- Spawn a sanctuary effect near the player -- local effect = core.magic.effects.records[core.magic.EFFECT_TYPE.Sanctuary] From 4a3ffb2073451a50b1505809625e820cd4b3510c Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 12 Apr 2025 17:44:25 +0200 Subject: [PATCH 06/23] Use camel case for variables --- apps/openmw/mwmechanics/aiwander.cpp | 36 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 3cc7aac838..447140d8e3 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -29,17 +29,6 @@ namespace MWMechanics { - static const int COUNT_BEFORE_RESET = 10; - static const float IDLE_POSITION_CHECK_INTERVAL = 1.5f; - - // to prevent overcrowding - static const int DESTINATION_TOLERANCE = 64; - - // distance must be long enough that NPC will need to move to get there. - static const int MINIMUM_WANDER_DISTANCE = DESTINATION_TOLERANCE * 2; - - static const std::size_t MAX_IDLE_SIZE = 8; - const std::string_view AiWander::sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1] = { "idle2", "idle3", @@ -53,11 +42,22 @@ namespace MWMechanics namespace { + constexpr int countBeforeReset = 10; + constexpr float idlePositionCheckInterval = 1.5f; + + // to prevent overcrowding + constexpr int destinationTolerance = 64; + + // distance must be long enough that NPC will need to move to get there. + constexpr int minimumWanderDistance = destinationTolerance * 2; + + constexpr std::size_t maxIdleSize = 8; + inline int getCountBeforeReset(const MWWorld::ConstPtr& actor) { if (actor.getClass().isPureWaterCreature(actor) || actor.getClass().isPureFlyingCreature(actor)) return 1; - return COUNT_BEFORE_RESET; + return countBeforeReset; } osg::Vec3f getRandomPointAround(const osg::Vec3f& position, const float distance) @@ -99,12 +99,12 @@ namespace MWMechanics std::vector getInitialIdle(const std::vector& idle) { - std::vector result(MAX_IDLE_SIZE, 0); - std::copy_n(idle.begin(), std::min(MAX_IDLE_SIZE, idle.size()), result.begin()); + std::vector result(maxIdleSize, 0); + std::copy_n(idle.begin(), std::min(maxIdleSize, idle.size()), result.begin()); return result; } - std::vector getInitialIdle(const unsigned char (&idle)[MAX_IDLE_SIZE]) + std::vector getInitialIdle(const unsigned char (&idle)[maxIdleSize]) { return std::vector(std::begin(idle), std::end(idle)); } @@ -494,7 +494,7 @@ namespace MWMechanics // Check if an idle actor is too far from all allowed nodes or too close to a door - if so start walking. storage.mCheckIdlePositionTimer += duration; - if (storage.mCheckIdlePositionTimer >= IDLE_POSITION_CHECK_INTERVAL && !isStationary()) + if (storage.mCheckIdlePositionTimer >= idlePositionCheckInterval && !isStationary()) { storage.mCheckIdlePositionTimer = 0; // restart timer static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 1.6f; @@ -535,7 +535,7 @@ namespace MWMechanics // Is there no destination or are we there yet? if ((!mPathFinder.isPathConstructed()) || pathTo(actor, osg::Vec3f(mPathFinder.getPath().back()), duration, supportedMovementDirections, - DESTINATION_TOLERANCE)) + destinationTolerance)) { stopWalking(actor); storage.setState(AiWanderStorage::Wander_ChooseAction); @@ -915,7 +915,7 @@ namespace MWMechanics float length = delta.length(); delta.normalize(); - int distance = std::max(mDistance / 2, MINIMUM_WANDER_DISTANCE); + int distance = std::max(mDistance / 2, minimumWanderDistance); // must not travel longer than distance between waypoints or NPC goes past waypoint distance = std::min(distance, static_cast(length)); From 20bd1491a784b19dba1b2f22cb473555a2ebbfea Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 12 Apr 2025 17:38:51 +0200 Subject: [PATCH 07/23] Make sure distance and duration are not negative --- apps/openmw/mwmechanics/aiwander.cpp | 19 ++++++++----------- apps/openmw/mwmechanics/aiwander.hpp | 4 ++-- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 447140d8e3..3dacbd167e 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -46,10 +46,10 @@ namespace MWMechanics constexpr float idlePositionCheckInterval = 1.5f; // to prevent overcrowding - constexpr int destinationTolerance = 64; + constexpr unsigned destinationTolerance = 64; // distance must be long enough that NPC will need to move to get there. - constexpr int minimumWanderDistance = destinationTolerance * 2; + constexpr unsigned minimumWanderDistance = destinationTolerance * 2; constexpr std::size_t maxIdleSize = 8; @@ -128,8 +128,8 @@ namespace MWMechanics AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat) : TypedAiPackage(repeat) - , mDistance(std::max(0, distance)) - , mDuration(std::max(0, duration)) + , mDistance(static_cast(std::max(0, distance))) + , mDuration(static_cast(std::max(0, duration))) , mRemainingDuration(duration) , mTimeOfDay(timeOfDay) , mIdle(getInitialIdle(idle)) @@ -259,9 +259,6 @@ namespace MWMechanics bool AiWander::reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos) { - if (mDistance <= 0) - storage.mCanWanderAlongPathGrid = false; - if (isPackageCompleted()) { stopWalking(actor); @@ -915,10 +912,10 @@ namespace MWMechanics float length = delta.length(); delta.normalize(); - int distance = std::max(mDistance / 2, minimumWanderDistance); + unsigned distance = std::max(mDistance / 2, minimumWanderDistance); // must not travel longer than distance between waypoints or NPC goes past waypoint - distance = std::min(distance, static_cast(length)); + distance = std::min(distance, static_cast(length)); delta *= distance; storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(vectorStart + delta)); } @@ -970,8 +967,8 @@ namespace MWMechanics AiWander::AiWander(const ESM::AiSequence::AiWander* wander) : TypedAiPackage(makeDefaultOptions().withRepeat(wander->mData.mShouldRepeat != 0)) - , mDistance(std::max(static_cast(0), wander->mData.mDistance)) - , mDuration(std::max(static_cast(0), wander->mData.mDuration)) + , mDistance(static_cast(std::max(static_cast(0), wander->mData.mDistance))) + , mDuration(static_cast(std::max(static_cast(0), wander->mData.mDuration))) , mRemainingDuration(wander->mDurationData.mRemainingDuration) , mTimeOfDay(wander->mData.mTimeOfDay) , mIdle(getInitialIdle(wander->mData.mIdle)) diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index f08980ad29..a1474bce5f 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -147,8 +147,8 @@ namespace MWMechanics void completeManualWalking(const MWWorld::Ptr& actor, AiWanderStorage& storage); bool isNearAllowedNode(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const; - const int mDistance; // how far the actor can wander from the spawn point - const int mDuration; + const unsigned mDistance; // how far the actor can wander from the spawn point + const unsigned mDuration; float mRemainingDuration; const int mTimeOfDay; const std::vector mIdle; From c79b39cf0d59e5c8c119e04daa4056467bb2de6a Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 12 Apr 2025 12:41:10 +0200 Subject: [PATCH 08/23] Remove unused arguments from isAreaOccupiedByOtherActor --- apps/openmw/mwbase/world.hpp | 4 ++-- apps/openmw/mwmechanics/obstacle.cpp | 10 ++-------- apps/openmw/mwmechanics/obstacle.hpp | 5 +---- .../mwphysics/hasspherecollisioncallback.hpp | 17 +++++------------ apps/openmw/mwphysics/physicssystem.cpp | 17 +++-------------- apps/openmw/mwphysics/physicssystem.hpp | 4 ++-- apps/openmw/mwworld/worldimp.cpp | 6 +++--- apps/openmw/mwworld/worldimp.hpp | 4 ++-- 8 files changed, 20 insertions(+), 47 deletions(-) diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index f268ed0e52..9016e23c0e 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -574,8 +574,8 @@ namespace MWBase virtual bool hasCollisionWithDoor( const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0; - virtual bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, - std::span ignore, std::vector* occupyingActors = nullptr) const = 0; + virtual bool isAreaOccupiedByOtherActor( + const osg::Vec3f& position, float radius, std::span ignore) const = 0; virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; diff --git a/apps/openmw/mwmechanics/obstacle.cpp b/apps/openmw/mwmechanics/obstacle.cpp index a6eb4f9c24..1112bc49d6 100644 --- a/apps/openmw/mwmechanics/obstacle.cpp +++ b/apps/openmw/mwmechanics/obstacle.cpp @@ -106,19 +106,13 @@ namespace MWMechanics return visitor.mResult; } - bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, bool ignorePlayer, - std::vector* occupyingActors) + bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination) { const auto world = MWBase::Environment::get().getWorld(); const osg::Vec3f halfExtents = world->getPathfindingAgentBounds(actor).mHalfExtents; const auto maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); - if (ignorePlayer) - { - const std::array ignore{ actor, world->getPlayerConstPtr() }; - return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore, occupyingActors); - } const std::array ignore{ actor }; - return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore, occupyingActors); + return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore); } ObstacleCheck::ObstacleCheck() diff --git a/apps/openmw/mwmechanics/obstacle.hpp b/apps/openmw/mwmechanics/obstacle.hpp index 532bc91331..f3214210b5 100644 --- a/apps/openmw/mwmechanics/obstacle.hpp +++ b/apps/openmw/mwmechanics/obstacle.hpp @@ -5,8 +5,6 @@ #include -#include - namespace MWWorld { class Ptr; @@ -24,8 +22,7 @@ namespace MWMechanics /** \return Pointer to the door, or empty pointer if none exists **/ const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist); - bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, - bool ignorePlayer = false, std::vector* occupyingActors = nullptr); + bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination); class ObstacleCheck { diff --git a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp index c1fa0ad1ee..a6563c731a 100644 --- a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp +++ b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp @@ -10,7 +10,7 @@ namespace MWPhysics { // https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_collision_detection - bool testAabbAgainstSphere( + inline bool testAabbAgainstSphere( const btVector3& aabbMin, const btVector3& aabbMax, const btVector3& position, const btScalar radius) { const btVector3 nearest(std::clamp(position.x(), aabbMin.x(), aabbMax.x()), @@ -18,35 +18,29 @@ namespace MWPhysics return nearest.distance(position) < radius; } - template + template class HasSphereCollisionCallback final : public btBroadphaseAabbCallback { public: - HasSphereCollisionCallback(const btVector3& position, const btScalar radius, const int mask, const int group, - const Ignore& ignore, OnCollision* onCollision) + explicit HasSphereCollisionCallback( + const btVector3& position, const btScalar radius, const int mask, const int group, const Ignore& ignore) : mPosition(position) , mRadius(radius) , mIgnore(ignore) , mCollisionFilterMask(mask) , mCollisionFilterGroup(group) - , mOnCollision(onCollision) { } bool process(const btBroadphaseProxy* proxy) override { - if (mResult && mOnCollision == nullptr) + if (mResult) return false; const auto collisionObject = static_cast(proxy->m_clientObject); if (mIgnore(collisionObject) || !needsCollision(*proxy) || !testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius)) return true; mResult = true; - if (mOnCollision != nullptr) - { - (*mOnCollision)(collisionObject); - return true; - } return !mResult; } @@ -58,7 +52,6 @@ namespace MWPhysics Ignore mIgnore; int mCollisionFilterMask; int mCollisionFilterGroup; - OnCollision* mOnCollision; bool mResult = false; bool needsCollision(const btBroadphaseProxy& proxy) const diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 5e7c70788d..55a0453bf8 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -849,8 +849,8 @@ namespace MWPhysics mWaterCollisionObject.get(), CollisionType_Water, CollisionType_Actor | CollisionType_Projectile); } - bool PhysicsSystem::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, - std::span ignore, std::vector* occupyingActors) const + bool PhysicsSystem::isAreaOccupiedByOtherActor( + const osg::Vec3f& position, const float radius, std::span ignore) const { std::vector ignoredObjects; ignoredObjects.reserve(ignore.size()); @@ -867,18 +867,7 @@ namespace MWPhysics const auto aabbMax = bulletPosition + btVector3(radius, radius, radius); const int mask = MWPhysics::CollisionType_Actor; const int group = MWPhysics::CollisionType_AnyPhysical; - if (occupyingActors == nullptr) - { - HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter, - static_cast(nullptr)); - mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); - return callback.getResult(); - } - const auto onCollision = [&](const btCollisionObject* object) { - if (PtrHolder* holder = static_cast(object->getUserPointer())) - occupyingActors->push_back(holder->getPtr()); - }; - HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter, &onCollision); + HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter); mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); return callback.getResult(); } diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index 546d72676e..92b37e105a 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -281,8 +281,8 @@ namespace MWPhysics std::for_each(mAnimatedObjects.begin(), mAnimatedObjects.end(), function); } - bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, - std::span ignore, std::vector* occupyingActors) const; + bool isAreaOccupiedByOtherActor( + const osg::Vec3f& position, float radius, std::span ignore) const; void reportStats(unsigned int frameNumber, osg::Stats& stats) const; void reportCollision(const btVector3& position, const btVector3& normal); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 97788669d5..099c6e403a 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -3832,10 +3832,10 @@ namespace MWWorld return btRayAabb(localFrom, localTo, aabbMin, aabbMax, hitDistance, hitNormal); } - bool World::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, - std::span ignore, std::vector* occupyingActors) const + bool World::isAreaOccupiedByOtherActor( + const osg::Vec3f& position, const float radius, std::span ignore) const { - return mPhysics->isAreaOccupiedByOtherActor(position, radius, ignore, occupyingActors); + return mPhysics->isAreaOccupiedByOtherActor(position, radius, ignore); } void World::reportStats(unsigned int frameNumber, osg::Stats& stats) const diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index b1286d5532..cce9d45e55 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -656,8 +656,8 @@ namespace MWWorld bool hasCollisionWithDoor( const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const override; - bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, - std::span ignore, std::vector* occupyingActors) const override; + bool isAreaOccupiedByOtherActor( + const osg::Vec3f& position, float radius, std::span ignore) const override; void reportStats(unsigned int frameNumber, osg::Stats& stats) const override; From ae909d7685febf8ecee6ac3ed823f19a3bcf4073 Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 12 Apr 2025 12:54:07 +0200 Subject: [PATCH 09/23] Remove isAreaOccupiedByOtherActor from obstacle.* It uses functions only from World anyway. --- apps/openmw/mwbase/world.hpp | 3 +-- apps/openmw/mwmechanics/aiwander.cpp | 16 +++++++------ apps/openmw/mwmechanics/obstacle.cpp | 9 -------- apps/openmw/mwmechanics/obstacle.hpp | 2 -- .../mwphysics/hasspherecollisioncallback.hpp | 9 ++++---- apps/openmw/mwphysics/physicssystem.cpp | 23 +++++++------------ apps/openmw/mwphysics/physicssystem.hpp | 2 +- apps/openmw/mwworld/worldimp.cpp | 7 +++--- apps/openmw/mwworld/worldimp.hpp | 3 +-- 9 files changed, 28 insertions(+), 46 deletions(-) diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 9016e23c0e..fea90171ed 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -574,8 +574,7 @@ namespace MWBase virtual bool hasCollisionWithDoor( const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0; - virtual bool isAreaOccupiedByOtherActor( - const osg::Vec3f& position, float radius, std::span ignore) const = 0; + virtual bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const = 0; virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 3dacbd167e..5112979c88 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -279,7 +279,9 @@ namespace MWMechanics getAllowedNodes(actor, storage); } - auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + MWBase::World& world = *MWBase::Environment::get().getWorld(); + + auto& prng = world.getPrng(); if (canActorMoveByZAxis(actor) && mDistance > 0) { // Typically want to idle for a short time before the next wander @@ -333,7 +335,7 @@ namespace MWMechanics if (storage.mIsWanderingManually && storage.mState == AiWanderStorage::Wander_Walking && (mPathFinder.getPathSize() == 0 || isDestinationHidden(actor, mPathFinder.getPath().back()) - || isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back()))) + || world.isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back()))) completeManualWalking(actor, storage); return false; // AiWander package not yet completed @@ -363,12 +365,12 @@ namespace MWMechanics std::size_t attempts = 10; // If a unit can't wander out of water, don't want to hang here const bool isWaterCreature = actor.getClass().isPureWaterCreature(actor); const bool isFlyingCreature = actor.getClass().isPureFlyingCreature(actor); - const auto world = MWBase::Environment::get().getWorld(); - const auto agentBounds = world->getPathfindingAgentBounds(actor); - const auto navigator = world->getNavigator(); + MWBase::World& world = *MWBase::Environment::get().getWorld(); + const auto agentBounds = world.getPathfindingAgentBounds(actor); + const auto navigator = world.getNavigator(); const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); - auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + Misc::Rng::Generator& prng = world.getPrng(); do { @@ -408,7 +410,7 @@ namespace MWMechanics if (isDestinationHidden(actor, mDestination)) continue; - if (isAreaOccupiedByOtherActor(actor, mDestination)) + if (world.isAreaOccupiedByOtherActor(actor, mDestination)) continue; constexpr float endTolerance = 0; diff --git a/apps/openmw/mwmechanics/obstacle.cpp b/apps/openmw/mwmechanics/obstacle.cpp index 1112bc49d6..9a66eafb51 100644 --- a/apps/openmw/mwmechanics/obstacle.cpp +++ b/apps/openmw/mwmechanics/obstacle.cpp @@ -106,15 +106,6 @@ namespace MWMechanics return visitor.mResult; } - bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination) - { - const auto world = MWBase::Environment::get().getWorld(); - const osg::Vec3f halfExtents = world->getPathfindingAgentBounds(actor).mHalfExtents; - const auto maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); - const std::array ignore{ actor }; - return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore); - } - ObstacleCheck::ObstacleCheck() : mEvadeDirectionIndex(std::size(evadeDirections) - 1) { diff --git a/apps/openmw/mwmechanics/obstacle.hpp b/apps/openmw/mwmechanics/obstacle.hpp index f3214210b5..a1c973765f 100644 --- a/apps/openmw/mwmechanics/obstacle.hpp +++ b/apps/openmw/mwmechanics/obstacle.hpp @@ -22,8 +22,6 @@ namespace MWMechanics /** \return Pointer to the door, or empty pointer if none exists **/ const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist); - bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination); - class ObstacleCheck { public: diff --git a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp index a6563c731a..6270cd3083 100644 --- a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp +++ b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp @@ -18,12 +18,11 @@ namespace MWPhysics return nearest.distance(position) < radius; } - template class HasSphereCollisionCallback final : public btBroadphaseAabbCallback { public: - explicit HasSphereCollisionCallback( - const btVector3& position, const btScalar radius, const int mask, const int group, const Ignore& ignore) + explicit HasSphereCollisionCallback(const btVector3& position, const btScalar radius, const int mask, + const int group, const btCollisionObject* ignore) : mPosition(position) , mRadius(radius) , mIgnore(ignore) @@ -37,7 +36,7 @@ namespace MWPhysics if (mResult) return false; const auto collisionObject = static_cast(proxy->m_clientObject); - if (mIgnore(collisionObject) || !needsCollision(*proxy) + if (mIgnore == collisionObject || !needsCollision(*proxy) || !testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius)) return true; mResult = true; @@ -49,7 +48,7 @@ namespace MWPhysics private: btVector3 mPosition; btScalar mRadius; - Ignore mIgnore; + const btCollisionObject* mIgnore; int mCollisionFilterMask; int mCollisionFilterGroup; bool mResult = false; diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 55a0453bf8..f403f97c2f 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -850,24 +850,17 @@ namespace MWPhysics } bool PhysicsSystem::isAreaOccupiedByOtherActor( - const osg::Vec3f& position, const float radius, std::span ignore) const + const MWWorld::LiveCellRefBase* actor, const osg::Vec3f& position, const float radius) const { - std::vector ignoredObjects; - ignoredObjects.reserve(ignore.size()); - for (const auto& v : ignore) - if (const auto it = mActors.find(v.mRef); it != mActors.end()) - ignoredObjects.push_back(it->second->getCollisionObject()); - std::sort(ignoredObjects.begin(), ignoredObjects.end()); - ignoredObjects.erase(std::unique(ignoredObjects.begin(), ignoredObjects.end()), ignoredObjects.end()); - const auto ignoreFilter = [&](const btCollisionObject* v) { - return std::binary_search(ignoredObjects.begin(), ignoredObjects.end(), v); - }; - const auto bulletPosition = Misc::Convert::toBullet(position); - const auto aabbMin = bulletPosition - btVector3(radius, radius, radius); - const auto aabbMax = bulletPosition + btVector3(radius, radius, radius); + const btCollisionObject* ignoredObject = nullptr; + if (const auto it = mActors.find(actor); it != mActors.end()) + ignoredObject = it->second->getCollisionObject(); + const btVector3 bulletPosition = Misc::Convert::toBullet(position); + const btVector3 aabbMin = bulletPosition - btVector3(radius, radius, radius); + const btVector3 aabbMax = bulletPosition + btVector3(radius, radius, radius); const int mask = MWPhysics::CollisionType_Actor; const int group = MWPhysics::CollisionType_AnyPhysical; - HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter); + HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoredObject); mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); return callback.getResult(); } diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index 92b37e105a..8a845b4c41 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -282,7 +282,7 @@ namespace MWPhysics } bool isAreaOccupiedByOtherActor( - const osg::Vec3f& position, float radius, std::span ignore) const; + const MWWorld::LiveCellRefBase* actor, const osg::Vec3f& position, float radius) const; void reportStats(unsigned int frameNumber, osg::Stats& stats) const; void reportCollision(const btVector3& position, const btVector3& normal); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 099c6e403a..c8564a07b5 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -3832,10 +3832,11 @@ namespace MWWorld return btRayAabb(localFrom, localTo, aabbMin, aabbMax, hitDistance, hitNormal); } - bool World::isAreaOccupiedByOtherActor( - const osg::Vec3f& position, const float radius, std::span ignore) const + bool World::isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const { - return mPhysics->isAreaOccupiedByOtherActor(position, radius, ignore); + const osg::Vec3f halfExtents = getPathfindingAgentBounds(actor).mHalfExtents; + const float maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); + return mPhysics->isAreaOccupiedByOtherActor(actor.mRef, position, 2 * maxHalfExtent); } void World::reportStats(unsigned int frameNumber, osg::Stats& stats) const diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index cce9d45e55..ac5e315a58 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -656,8 +656,7 @@ namespace MWWorld bool hasCollisionWithDoor( const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const override; - bool isAreaOccupiedByOtherActor( - const osg::Vec3f& position, float radius, std::span ignore) const override; + bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const override; void reportStats(unsigned int frameNumber, osg::Stats& stats) const override; From 7c46635a5a9526713be8d9aa8ebb60460fab7835 Mon Sep 17 00:00:00 2001 From: elsid Date: Mon, 21 Apr 2025 13:59:36 +0200 Subject: [PATCH 10/23] Make trimAllowedNodes a free function --- apps/openmw/mwmechanics/aiwander.cpp | 52 ++++++++++++++-------------- apps/openmw/mwmechanics/aiwander.hpp | 2 -- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 5112979c88..2bdeb27179 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -109,6 +109,31 @@ namespace MWMechanics return std::vector(std::begin(idle), std::end(idle)); } + void trimAllowedNodes(const std::deque& path, std::vector& nodes) + { + // TODO: how to add these back in once the door opens? + // Idea: keep a list of detected closed doors (see aicombat.cpp) + // Every now and then check whether one of the doors is opened. (maybe + // at the end of playing idle?) If the door is opened then re-calculate + // allowed nodes starting from the spawn point. + std::vector points(path.begin(), path.end()); + while (points.size() >= 2) + { + const osg::Vec3f point = points.back(); + for (std::size_t j = 0; j < nodes.size(); j++) + { + // FIXME: doesn't handle a door with the same X/Y + // coordinates but with a different Z + if (std::abs(nodes[j].mX - point.x()) <= 0.5 && std::abs(nodes[j].mY - point.y()) <= 0.5) + { + nodes.erase(nodes.begin() + j); + break; + } + } + points.pop_back(); + } + } + } AiWanderStorage::AiWanderStorage() @@ -586,7 +611,7 @@ namespace MWMechanics { // remove allowed points then select another random destination storage.mTrimCurrentNode = true; - trimAllowedNodes(storage.mAllowedNodes, mPathFinder); + trimAllowedNodes(mPathFinder.getPath(), storage.mAllowedNodes); mObstacleCheck.clear(); stopWalking(actor); storage.setState(AiWanderStorage::Wander_MoveNow); @@ -643,31 +668,6 @@ namespace MWMechanics storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode); } - void AiWander::trimAllowedNodes(std::vector& nodes, const PathFinder& pathfinder) - { - // TODO: how to add these back in once the door opens? - // Idea: keep a list of detected closed doors (see aicombat.cpp) - // Every now and then check whether one of the doors is opened. (maybe - // at the end of playing idle?) If the door is opened then re-calculate - // allowed nodes starting from the spawn point. - auto paths = pathfinder.getPath(); - while (paths.size() >= 2) - { - const auto pt = paths.back(); - for (unsigned int j = 0; j < nodes.size(); j++) - { - // FIXME: doesn't handle a door with the same X/Y - // coordinates but with a different Z - if (std::abs(nodes[j].mX - pt.x()) <= 0.5 && std::abs(nodes[j].mY - pt.y()) <= 0.5) - { - nodes.erase(nodes.begin() + j); - break; - } - } - paths.pop_back(); - } - } - void AiWander::stopWalking(const MWWorld::Ptr& actor) { mPathFinder.clearPath(); diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index a1474bce5f..0bb0f64a83 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -166,8 +166,6 @@ namespace MWMechanics void getAllowedNodes(const MWWorld::Ptr& actor, AiWanderStorage& storage); - void trimAllowedNodes(std::vector& nodes, const PathFinder& pathfinder); - // constants for converting idleSelect values into groupNames enum GroupIndex { From d3328552e8076059062907f4d6ae6f241cecb0bc Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Mon, 2 Jun 2025 15:23:43 +0300 Subject: [PATCH 11/23] Avoid recomputing collision prediction things an extra N times (#8548) --- apps/openmw/mwmechanics/actors.cpp | 35 +++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 3ba6bfdc8d..69108f9aff 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -1326,19 +1326,37 @@ namespace MWMechanics const MWWorld::Ptr player = getPlayer(); const MWBase::World* const world = MWBase::Environment::get().getWorld(); + + struct CacheEntry + { + MWWorld::Ptr mPtr; + float mMaxSpeed; + osg::Vec3f mHalfExtents; + Movement& mMovement; + }; + + std::vector cache; + cache.reserve(mActors.size()); for (const Actor& actor : mActors) { if (actor.isInvalid()) continue; const MWWorld::Ptr& ptr = actor.getPtr(); + const MWWorld::Class& cls = ptr.getClass(); + cache.push_back({ ptr, cls.getMaxSpeed(ptr), world->getHalfExtents(ptr), cls.getMovementSettings(ptr) }); + } + + for (const CacheEntry& cached : cache) + { + const MWWorld::Ptr& ptr = cached.mPtr; if (ptr == player) continue; // Don't interfere with player controls. - const float maxSpeed = ptr.getClass().getMaxSpeed(ptr); + const float maxSpeed = cached.mMaxSpeed; if (maxSpeed == 0.0) continue; // Can't move, so there is no sense to predict collisions. - Movement& movement = ptr.getClass().getMovementSettings(ptr); + Movement& movement = cached.mMovement; const osg::Vec2f origMovement(movement.mPosition[0], movement.mPosition[1]); const bool isMoving = origMovement.length2() > 0.01; if (movement.mPosition[1] < 0) @@ -1379,7 +1397,7 @@ namespace MWMechanics const osg::Vec2f baseSpeed = origMovement * maxSpeed; const osg::Vec3f basePos = ptr.getRefData().getPosition().asVec3(); const float baseRotZ = ptr.getRefData().getPosition().rot[2]; - const osg::Vec3f halfExtents = world->getHalfExtents(ptr); + const osg::Vec3f& halfExtents = cached.mHalfExtents; const float maxDistToCheck = isMoving ? maxDistForPartialAvoiding : maxDistForStrictAvoiding; float timeToCheck = maxTimeToCheck; @@ -1392,15 +1410,13 @@ namespace MWMechanics float angleToApproachingActor = 0; // Iterate through all other actors and predict collisions. - for (const Actor& otherActor : mActors) + for (const CacheEntry& otherCached : cache) { - if (otherActor.isInvalid()) - continue; - const MWWorld::Ptr& otherPtr = otherActor.getPtr(); + const MWWorld::Ptr& otherPtr = otherCached.mPtr; if (otherPtr == ptr || otherPtr == currentTarget) continue; - const osg::Vec3f otherHalfExtents = world->getHalfExtents(otherPtr); + const osg::Vec3f& otherHalfExtents = otherCached.mHalfExtents; const osg::Vec3f deltaPos = otherPtr.getRefData().getPosition().asVec3() - basePos; const osg::Vec2f relPos = Misc::rotateVec2f(osg::Vec2f(deltaPos.x(), deltaPos.y()), baseRotZ); const float dist = deltaPos.length(); @@ -1413,8 +1429,7 @@ namespace MWMechanics if (deltaPos.z() > halfExtents.z() * 2 || deltaPos.z() < -otherHalfExtents.z() * 2) continue; - const osg::Vec3f speed = otherPtr.getClass().getMovementSettings(otherPtr).asVec3() - * otherPtr.getClass().getMaxSpeed(otherPtr); + const osg::Vec3f speed = otherCached.mMovement.asVec3() * otherCached.mMaxSpeed; const float rotZ = otherPtr.getRefData().getPosition().rot[2]; const osg::Vec2f relSpeed = Misc::rotateVec2f(osg::Vec2f(speed.x(), speed.y()), baseRotZ - rotZ) - baseSpeed; From 50f5bc51c68ca6d7bda64fc93f1859df2d9ca836 Mon Sep 17 00:00:00 2001 From: elsid Date: Mon, 21 Apr 2025 14:51:41 +0200 Subject: [PATCH 12/23] Store allowed positions as osg::Vec3f There is no need to store pathgrid points. --- apps/openmw/mwmechanics/aiwander.cpp | 168 ++++++++++++------------ apps/openmw/mwmechanics/aiwander.hpp | 31 ++--- apps/openmw/mwmechanics/pathfinding.hpp | 13 -- components/misc/coordinateconverter.hpp | 12 +- 4 files changed, 108 insertions(+), 116 deletions(-) diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 2bdeb27179..a2d304c2e0 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -109,24 +109,25 @@ namespace MWMechanics return std::vector(std::begin(idle), std::end(idle)); } - void trimAllowedNodes(const std::deque& path, std::vector& nodes) + void trimAllowedPositions(const std::deque& path, std::vector& allowedPositions) { // TODO: how to add these back in once the door opens? // Idea: keep a list of detected closed doors (see aicombat.cpp) // Every now and then check whether one of the doors is opened. (maybe // at the end of playing idle?) If the door is opened then re-calculate - // allowed nodes starting from the spawn point. + // allowed positions starting from the spawn point. std::vector points(path.begin(), path.end()); while (points.size() >= 2) { const osg::Vec3f point = points.back(); - for (std::size_t j = 0; j < nodes.size(); j++) + for (std::size_t j = 0; j < allowedPositions.size(); j++) { // FIXME: doesn't handle a door with the same X/Y // coordinates but with a different Z - if (std::abs(nodes[j].mX - point.x()) <= 0.5 && std::abs(nodes[j].mY - point.y()) <= 0.5) + if (std::abs(allowedPositions[j].x() - point.x()) <= 0.5 + && std::abs(allowedPositions[j].y() - point.y()) <= 0.5) { - nodes.erase(nodes.begin() + j); + allowedPositions.erase(allowedPositions.begin() + j); break; } } @@ -143,9 +144,9 @@ namespace MWMechanics , mCanWanderAlongPathGrid(true) , mIdleAnimation(0) , mBadIdles() - , mPopulateAvailableNodes(true) - , mAllowedNodes() - , mTrimCurrentNode(false) + , mPopulateAvailablePositions(true) + , mAllowedPositions() + , mTrimCurrentPosition(false) , mCheckIdlePositionTimer(0) , mStuckCount(0) { @@ -298,10 +299,10 @@ namespace MWMechanics mStoredInitialActorPosition = true; } - // Initialization to discover & store allowed node points for this actor. - if (storage.mPopulateAvailableNodes) + // Initialization to discover & store allowed positions points for this actor. + if (storage.mPopulateAvailablePositions) { - getAllowedNodes(actor, storage); + fillAllowedPositions(actor, storage); } MWBase::World& world = *MWBase::Environment::get().getWorld(); @@ -319,7 +320,7 @@ namespace MWMechanics } // If the package has a wander distance but no pathgrid is available, // randomly idle or wander near spawn point - else if (storage.mAllowedNodes.empty() && mDistance > 0 && !storage.mIsWanderingManually) + else if (storage.mAllowedPositions.empty() && mDistance > 0 && !storage.mIsWanderingManually) { // Typically want to idle for a short time before the next wander if (Misc::Rng::rollDice(100, prng) >= 96) @@ -331,7 +332,7 @@ namespace MWMechanics storage.setState(AiWanderStorage::Wander_IdleNow); } } - else if (storage.mAllowedNodes.empty() && !storage.mIsWanderingManually) + else if (storage.mAllowedPositions.empty() && !storage.mIsWanderingManually) { storage.mCanWanderAlongPathGrid = false; } @@ -347,9 +348,9 @@ namespace MWMechanics // Construct a new path if there isn't one if (!mPathFinder.isPathConstructed()) { - if (!storage.mAllowedNodes.empty()) + if (!storage.mAllowedPositions.empty()) { - setPathToAnAllowedNode(actor, storage, pos); + setPathToAnAllowedPosition(actor, storage, pos); } } } @@ -515,17 +516,17 @@ namespace MWMechanics void AiWander::onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage) { - // Check if an idle actor is too far from all allowed nodes or too close to a door - if so start walking. + // Check if an idle actor is too far from all allowed positions or too close to a door - if so start walking. storage.mCheckIdlePositionTimer += duration; if (storage.mCheckIdlePositionTimer >= idlePositionCheckInterval && !isStationary()) { storage.mCheckIdlePositionTimer = 0; // restart timer static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 1.6f; - if (proximityToDoor(actor, distance) || !isNearAllowedNode(actor, storage, distance)) + if (proximityToDoor(actor, distance) || !isNearAllowedPosition(actor, storage, distance)) { storage.setState(AiWanderStorage::Wander_MoveNow); - storage.mTrimCurrentNode = false; // just in case + storage.mTrimCurrentPosition = false; // just in case return; } } @@ -541,16 +542,14 @@ namespace MWMechanics } } - bool AiWander::isNearAllowedNode(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const + bool AiWander::isNearAllowedPosition( + const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const { const osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); - for (const ESM::Pathgrid::Point& node : storage.mAllowedNodes) - { - osg::Vec3f point(node.mX, node.mY, node.mZ); - if ((actorPos - point).length2() < distance * distance) - return true; - } - return false; + const float squaredDistance = distance * distance; + return std::ranges::find_if(storage.mAllowedPositions, [&](const osg::Vec3& v) { + return (actorPos - v).length2() < squaredDistance; + }) != storage.mAllowedPositions.end(); } void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, @@ -610,8 +609,8 @@ namespace MWMechanics if (proximityToDoor(actor, distance)) { // remove allowed points then select another random destination - storage.mTrimCurrentNode = true; - trimAllowedNodes(mPathFinder.getPath(), storage.mAllowedNodes); + storage.mTrimCurrentPosition = true; + trimAllowedPositions(mPathFinder.getPath(), storage.mAllowedPositions); mObstacleCheck.clear(); stopWalking(actor); storage.setState(AiWanderStorage::Wander_MoveNow); @@ -630,42 +629,41 @@ namespace MWMechanics } } - void AiWander::setPathToAnAllowedNode( + void AiWander::setPathToAnAllowedPosition( const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos) { auto world = MWBase::Environment::get().getWorld(); auto& prng = world->getPrng(); - unsigned int randNode = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng); - const ESM::Pathgrid::Point& dest = storage.mAllowedNodes[randNode]; + const std::size_t randomAllowedPositionIndex + = static_cast(Misc::Rng::rollDice(storage.mAllowedPositions.size(), prng)); + const osg::Vec3f randomAllowedPosition = storage.mAllowedPositions[randomAllowedPositionIndex]; const osg::Vec3f start = actorPos.asVec3(); // don't take shortcuts for wandering const ESM::Pathgrid* pathgrid = world->getStore().get().search(*actor.getCell()->getCell()); - const osg::Vec3f destVec3f = PathFinder::makeOsgVec3(dest); - mPathFinder.buildPathByPathgrid(start, destVec3f, actor.getCell(), getPathGridGraph(pathgrid)); + mPathFinder.buildPathByPathgrid(start, randomAllowedPosition, actor.getCell(), getPathGridGraph(pathgrid)); if (mPathFinder.isPathConstructed()) { - mDestination = destVec3f; + mDestination = randomAllowedPosition; mHasDestination = true; mUsePathgrid = true; - // Remove this node as an option and add back the previously used node (stops NPC from picking the same - // node): - ESM::Pathgrid::Point temp = storage.mAllowedNodes[randNode]; - storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode); - // check if mCurrentNode was taken out of mAllowedNodes - if (storage.mTrimCurrentNode && storage.mAllowedNodes.size() > 1) - storage.mTrimCurrentNode = false; + // Remove this position as an option and add back the previously used position (stops NPC from picking the + // same position): + storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex); + // check if mCurrentPosition was taken out of mAllowedPositions + if (storage.mTrimCurrentPosition && storage.mAllowedPositions.size() > 1) + storage.mTrimCurrentPosition = false; else - storage.mAllowedNodes.push_back(storage.mCurrentNode); - storage.mCurrentNode = temp; + storage.mAllowedPositions.push_back(storage.mCurrentPosition); + storage.mCurrentPosition = randomAllowedPosition; storage.setState(AiWanderStorage::Wander_Walking); } - // Choose a different node and delete this one from possible nodes because it is uncreachable: + // Choose a different position and delete this one from possible positions because it is uncreachable: else - storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode); + storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex); } void AiWander::stopWalking(const MWWorld::Ptr& actor) @@ -741,20 +739,20 @@ namespace MWMechanics return; AiWanderStorage& storage = state.get(); - if (storage.mPopulateAvailableNodes) - getAllowedNodes(actor, storage); + if (storage.mPopulateAvailablePositions) + fillAllowedPositions(actor, storage); - if (storage.mAllowedNodes.empty()) + if (storage.mAllowedPositions.empty()) return; auto& prng = MWBase::Environment::get().getWorld()->getPrng(); - int index = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng); - ESM::Pathgrid::Point worldDest = storage.mAllowedNodes[index]; + int index = Misc::Rng::rollDice(storage.mAllowedPositions.size(), prng); + const osg::Vec3f worldDest = storage.mAllowedPositions[index]; const Misc::CoordinateConverter converter = Misc::makeCoordinateConverter(*actor.getCell()->getCell()); - ESM::Pathgrid::Point dest = converter.toLocalPoint(worldDest); + osg::Vec3f dest = converter.toLocalVec3(worldDest); - bool isPathGridOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange( - PathFinder::makeOsgVec3(worldDest), 60); + const bool isPathGridOccupied + = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(worldDest, 60); // add offset only if the selected pathgrid is occupied by another actor if (isPathGridOccupied) @@ -774,19 +772,17 @@ namespace MWMechanics const ESM::Pathgrid::Point& connDest = points[randomIndex]; // add an offset towards random neighboring node - osg::Vec3f dir = PathFinder::makeOsgVec3(connDest) - PathFinder::makeOsgVec3(dest); - float length = dir.length(); + osg::Vec3f dir = PathFinder::makeOsgVec3(connDest) - dest; + const float length = dir.length(); dir.normalize(); for (int j = 1; j <= 3; j++) { // move for 5-15% towards random neighboring node - dest - = PathFinder::makePathgridPoint(PathFinder::makeOsgVec3(dest) + dir * (j * 5 * length / 100.f)); - worldDest = converter.toWorldPoint(dest); + dest = dest + dir * (j * 5 * length / 100.f); isOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange( - PathFinder::makeOsgVec3(worldDest), 60); + converter.toWorldVec3(dest), 60); if (!isOccupied) break; @@ -806,19 +802,18 @@ namespace MWMechanics // place above to prevent moving inside objects, e.g. stairs, because a vector between pathgrids can be // underground. Adding 20 in adjustPosition() is not enough. - dest.mZ += 60; + dest.z() += 60; converter.toWorld(dest); state.reset(); - osg::Vec3f pos(static_cast(dest.mX), static_cast(dest.mY), static_cast(dest.mZ)); - MWBase::Environment::get().getWorld()->moveObject(actor, pos); + MWBase::Environment::get().getWorld()->moveObject(actor, dest); actor.getClass().adjustPosition(actor, false); } void AiWander::getNeighbouringNodes( - ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points) + const osg::Vec3f& dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points) { const ESM::Pathgrid* pathgrid = MWBase::Environment::get().getESMStore()->get().search(*currentCell->getCell()); @@ -826,19 +821,19 @@ namespace MWMechanics if (pathgrid == nullptr || pathgrid->mPoints.empty()) return; - size_t index = PathFinder::getClosestPoint(pathgrid, PathFinder::makeOsgVec3(dest)); + const size_t index = PathFinder::getClosestPoint(pathgrid, dest); getPathGridGraph(pathgrid).getNeighbouringPoints(index, points); } - void AiWander::getAllowedNodes(const MWWorld::Ptr& actor, AiWanderStorage& storage) + void AiWander::fillAllowedPositions(const MWWorld::Ptr& actor, AiWanderStorage& storage) { // infrequently used, therefore no benefit in caching it as a member const MWWorld::CellStore* cellStore = actor.getCell(); const ESM::Pathgrid* pathgrid = MWBase::Environment::get().getESMStore()->get().search(*cellStore->getCell()); - storage.mAllowedNodes.clear(); + storage.mAllowedPositions.clear(); // If there is no path this actor doesn't go anywhere. See: // https://forum.openmw.org/viewtopic.php?t=1556 @@ -861,32 +856,33 @@ namespace MWMechanics // Find closest pathgrid point size_t closestPointIndex = PathFinder::getClosestPoint(pathgrid, npcPos); - // mAllowedNodes for this actor with pathgrid point indexes based on mDistance + // mAllowedPositions for this actor with pathgrid point indexes based on mDistance // and if the point is connected to the closest current point // NOTE: mPoints is in local coordinates size_t pointIndex = 0; for (size_t counter = 0; counter < pathgrid->mPoints.size(); counter++) { - osg::Vec3f nodePos(PathFinder::makeOsgVec3(pathgrid->mPoints[counter])); + const osg::Vec3f nodePos = PathFinder::makeOsgVec3(pathgrid->mPoints[counter]); if ((npcPos - nodePos).length2() <= mDistance * mDistance && getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, counter)) { - storage.mAllowedNodes.push_back(converter.toWorldPoint(pathgrid->mPoints[counter])); + storage.mAllowedPositions.push_back( + Misc::Convert::makeOsgVec3f(converter.toWorldPoint(pathgrid->mPoints[counter]))); pointIndex = counter; } } - if (storage.mAllowedNodes.size() == 1) + if (storage.mAllowedPositions.size() == 1) { - storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(mInitialActorPosition)); + storage.mAllowedPositions.push_back(mInitialActorPosition); addNonPathGridAllowedPoints(pathgrid, pointIndex, storage, converter); } - if (!storage.mAllowedNodes.empty()) + if (!storage.mAllowedPositions.empty()) { - setCurrentNodeToClosestAllowedNode(storage); + setCurrentPositionToClosestAllowedPosition(storage); } } - storage.mPopulateAvailableNodes = false; + storage.mPopulateAvailablePositions = false; } // When only one path grid point in wander distance, @@ -900,13 +896,13 @@ namespace MWMechanics { if (edge.mV0 == pointIndex) { - AddPointBetweenPathGridPoints(converter.toWorldPoint(pathGrid->mPoints[edge.mV0]), + addPositionBetweenPathgridPoints(converter.toWorldPoint(pathGrid->mPoints[edge.mV0]), converter.toWorldPoint(pathGrid->mPoints[edge.mV1]), storage); } } } - void AiWander::AddPointBetweenPathGridPoints( + void AiWander::addPositionBetweenPathgridPoints( const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage) { osg::Vec3f vectorStart = PathFinder::makeOsgVec3(start); @@ -919,25 +915,25 @@ namespace MWMechanics // must not travel longer than distance between waypoints or NPC goes past waypoint distance = std::min(distance, static_cast(length)); delta *= distance; - storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(vectorStart + delta)); + storage.mAllowedPositions.push_back(vectorStart + delta); } - void AiWander::setCurrentNodeToClosestAllowedNode(AiWanderStorage& storage) + void AiWander::setCurrentPositionToClosestAllowedPosition(AiWanderStorage& storage) { - float distanceToClosestNode = std::numeric_limits::max(); + float distanceToClosestPosition = std::numeric_limits::max(); size_t index = 0; - for (size_t i = 0; i < storage.mAllowedNodes.size(); ++i) + for (size_t i = 0; i < storage.mAllowedPositions.size(); ++i) { - osg::Vec3f nodePos(PathFinder::makeOsgVec3(storage.mAllowedNodes[i])); - float tempDist = (mInitialActorPosition - nodePos).length2(); - if (tempDist < distanceToClosestNode) + const osg::Vec3f position = storage.mAllowedPositions[i]; + const float tempDist = (mInitialActorPosition - position).length2(); + if (tempDist < distanceToClosestPosition) { index = i; - distanceToClosestNode = tempDist; + distanceToClosestPosition = tempDist; } } - storage.mCurrentNode = storage.mAllowedNodes[index]; - storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + index); + storage.mCurrentPosition = storage.mAllowedPositions[index]; + storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + index); } void AiWander::writeState(ESM::AiSequence::AiSequence& sequence) const diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index 0bb0f64a83..0518015936 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -51,14 +51,13 @@ namespace MWMechanics unsigned short mIdleAnimation; std::vector mBadIdles; // Idle animations that when called cause errors - // do we need to calculate allowed nodes based on mDistance - bool mPopulateAvailableNodes; + bool mPopulateAvailablePositions; - // allowed pathgrid nodes based on mDistance from the spawn point - std::vector mAllowedNodes; + // allowed destination positions based on mDistance from the spawn point + std::vector mAllowedPositions; - ESM::Pathgrid::Point mCurrentNode; - bool mTrimCurrentNode; + osg::Vec3f mCurrentPosition; + bool mTrimCurrentPosition; float mCheckIdlePositionTimer; int mStuckCount; @@ -132,7 +131,8 @@ namespace MWMechanics bool playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); bool checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); int getRandomIdle() const; - void setPathToAnAllowedNode(const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos); + void setPathToAnAllowedPosition( + const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos); void evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage); void doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, MWWorld::MovementDirectionFlags supportedMovementDirections, AiWanderStorage& storage); @@ -145,26 +145,27 @@ namespace MWMechanics void wanderNearStart(const MWWorld::Ptr& actor, AiWanderStorage& storage, int wanderDistance); bool destinationIsAtWater(const MWWorld::Ptr& actor, const osg::Vec3f& destination); void completeManualWalking(const MWWorld::Ptr& actor, AiWanderStorage& storage); - bool isNearAllowedNode(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const; + bool isNearAllowedPosition(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const; - const unsigned mDistance; // how far the actor can wander from the spawn point + // how far the actor can wander from the spawn point + const unsigned mDistance; const unsigned mDuration; float mRemainingDuration; const int mTimeOfDay; const std::vector mIdle; bool mStoredInitialActorPosition; - osg::Vec3f - mInitialActorPosition; // Note: an original engine does not reset coordinates even when actor changes a cell + // Note: an original engine does not reset coordinates even when actor changes a cell + osg::Vec3f mInitialActorPosition; bool mHasDestination; osg::Vec3f mDestination; bool mUsePathgrid; void getNeighbouringNodes( - ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points); + const osg::Vec3f& dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points); - void getAllowedNodes(const MWWorld::Ptr& actor, AiWanderStorage& storage); + void fillAllowedPositions(const MWWorld::Ptr& actor, AiWanderStorage& storage); // constants for converting idleSelect values into groupNames enum GroupIndex @@ -173,12 +174,12 @@ namespace MWMechanics GroupIndex_MaxIdle = 9 }; - void setCurrentNodeToClosestAllowedNode(AiWanderStorage& storage); + void setCurrentPositionToClosestAllowedPosition(AiWanderStorage& storage); void addNonPathGridAllowedPoints(const ESM::Pathgrid* pathGrid, size_t pointIndex, AiWanderStorage& storage, const Misc::CoordinateConverter& converter); - void AddPointBetweenPathGridPoints( + void addPositionBetweenPathgridPoints( const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage); /// lookup table for converting idleSelect value to groupName diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index 94242404e4..b68532c9d4 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -145,19 +145,6 @@ namespace MWMechanics mPath.push_back(point); } - /// utility function to convert a osg::Vec3f to a Pathgrid::Point - static ESM::Pathgrid::Point makePathgridPoint(const osg::Vec3f& v) - { - return ESM::Pathgrid::Point(static_cast(v[0]), static_cast(v[1]), static_cast(v[2])); - } - - /// utility function to convert an ESM::Position to a Pathgrid::Point - static ESM::Pathgrid::Point makePathgridPoint(const ESM::Position& p) - { - return ESM::Pathgrid::Point( - static_cast(p.pos[0]), static_cast(p.pos[1]), static_cast(p.pos[2])); - } - static osg::Vec3f makeOsgVec3(const ESM::Pathgrid::Point& p) { return osg::Vec3f(static_cast(p.mX), static_cast(p.mY), static_cast(p.mZ)); diff --git a/components/misc/coordinateconverter.hpp b/components/misc/coordinateconverter.hpp index 7853880809..6c4d8dbf71 100644 --- a/components/misc/coordinateconverter.hpp +++ b/components/misc/coordinateconverter.hpp @@ -59,10 +59,18 @@ namespace Misc point.y() -= static_cast(mCellY); } + osg::Vec3f toWorldVec3(const osg::Vec3f& point) const + { + osg::Vec3f result = point; + toWorld(result); + return result; + } + osg::Vec3f toLocalVec3(const osg::Vec3f& point) const { - return osg::Vec3f( - point.x() - static_cast(mCellX), point.y() - static_cast(mCellY), point.z()); + osg::Vec3f result = point; + toLocal(result); + return result; } private: From 927b2bcceb65ad95c30af3f8a2f1ead524f65493 Mon Sep 17 00:00:00 2001 From: elsid Date: Mon, 21 Apr 2025 15:34:18 +0200 Subject: [PATCH 13/23] Replace PathFinder::makeOsgVec3 by Misc::Convert::makeOsgVec3f --- apps/openmw/mwmechanics/aicombat.cpp | 3 ++- apps/openmw/mwmechanics/aiwander.cpp | 8 ++++---- apps/openmw/mwmechanics/pathfinding.cpp | 8 ++++---- apps/openmw/mwmechanics/pathfinding.hpp | 8 ++------ 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index a6f9935194..a769d85cdd 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -455,7 +455,8 @@ namespace MWMechanics float dist = (actor.getRefData().getPosition().asVec3() - target.getRefData().getPosition().asVec3()).length(); if ((dist > fFleeDistance && !storage.mLOS) - || pathTo(actor, PathFinder::makeOsgVec3(storage.mFleeDest), duration, supportedMovementDirections)) + || pathTo( + actor, Misc::Convert::makeOsgVec3f(storage.mFleeDest), duration, supportedMovementDirections)) { state = AiCombatStorage::FleeState_Idle; } diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index a2d304c2e0..dfa0bc41ac 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -772,7 +772,7 @@ namespace MWMechanics const ESM::Pathgrid::Point& connDest = points[randomIndex]; // add an offset towards random neighboring node - osg::Vec3f dir = PathFinder::makeOsgVec3(connDest) - dest; + osg::Vec3f dir = Misc::Convert::makeOsgVec3f(connDest) - dest; const float length = dir.length(); dir.normalize(); @@ -862,7 +862,7 @@ namespace MWMechanics size_t pointIndex = 0; for (size_t counter = 0; counter < pathgrid->mPoints.size(); counter++) { - const osg::Vec3f nodePos = PathFinder::makeOsgVec3(pathgrid->mPoints[counter]); + const osg::Vec3f nodePos = Misc::Convert::makeOsgVec3f(pathgrid->mPoints[counter]); if ((npcPos - nodePos).length2() <= mDistance * mDistance && getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, counter)) { @@ -905,8 +905,8 @@ namespace MWMechanics void AiWander::addPositionBetweenPathgridPoints( const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage) { - osg::Vec3f vectorStart = PathFinder::makeOsgVec3(start); - osg::Vec3f delta = PathFinder::makeOsgVec3(end) - vectorStart; + osg::Vec3f vectorStart = Misc::Convert::makeOsgVec3f(start); + osg::Vec3f delta = Misc::Convert::makeOsgVec3f(end) - vectorStart; float length = delta.length(); delta.normalize(); diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index dc9d8e4061..dcad29c907 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -223,7 +223,7 @@ namespace MWMechanics { ESM::Pathgrid::Point temp(pathgrid->mPoints[startNode]); converter.toWorld(temp); - *out++ = makeOsgVec3(temp); + *out++ = Misc::Convert::makeOsgVec3f(temp); } else { @@ -234,8 +234,8 @@ namespace MWMechanics if (path.size() > 1) { ESM::Pathgrid::Point secondNode = *(++path.begin()); - osg::Vec3f firstNodeVec3f = makeOsgVec3(pathgrid->mPoints[startNode]); - osg::Vec3f secondNodeVec3f = makeOsgVec3(secondNode); + osg::Vec3f firstNodeVec3f = Misc::Convert::makeOsgVec3f(pathgrid->mPoints[startNode]); + osg::Vec3f secondNodeVec3f = Misc::Convert::makeOsgVec3f(secondNode); osg::Vec3f toSecondNodeVec3f = secondNodeVec3f - firstNodeVec3f; osg::Vec3f toStartPointVec3f = startPointInLocalCoords - firstNodeVec3f; if (toSecondNodeVec3f * toStartPointVec3f > 0) @@ -259,7 +259,7 @@ namespace MWMechanics // convert supplied path to world coordinates std::transform(path.begin(), path.end(), out, [&](ESM::Pathgrid::Point& point) { converter.toWorld(point); - return makeOsgVec3(point); + return Misc::Convert::makeOsgVec3f(point); }); } diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index b68532c9d4..20ba85cc96 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace MWWorld { @@ -145,18 +146,13 @@ namespace MWMechanics mPath.push_back(point); } - static osg::Vec3f makeOsgVec3(const ESM::Pathgrid::Point& p) - { - return osg::Vec3f(static_cast(p.mX), static_cast(p.mY), static_cast(p.mZ)); - } - // Slightly cheaper version for comparisons. // Caller needs to be careful for very short distances (i.e. less than 1) // or when accumuating the results i.e. (a + b)^2 != a^2 + b^2 // static float distanceSquared(const ESM::Pathgrid::Point& point, const osg::Vec3f& pos) { - return (MWMechanics::PathFinder::makeOsgVec3(point) - pos).length2(); + return (Misc::Convert::makeOsgVec3f(point) - pos).length2(); } // Return the closest pathgrid point index from the specified position From 3f1ac1848ca3be0a28e0b4eaf4e2e07ae345db1a Mon Sep 17 00:00:00 2001 From: elsid Date: Mon, 21 Apr 2025 15:50:07 +0200 Subject: [PATCH 14/23] Move getClosestPoint to a separate file --- apps/openmw/mwmechanics/aicombat.cpp | 16 ++++---- apps/openmw/mwmechanics/aicombat.hpp | 5 ++- apps/openmw/mwmechanics/aipackage.hpp | 2 + apps/openmw/mwmechanics/aiwander.cpp | 5 ++- apps/openmw/mwmechanics/aiwander.hpp | 11 ++--- apps/openmw/mwmechanics/pathfinding.cpp | 9 +++-- apps/openmw/mwmechanics/pathfinding.hpp | 42 +------------------- components/misc/pathgridutils.hpp | 53 +++++++++++++++++++++++++ 8 files changed, 81 insertions(+), 62 deletions(-) create mode 100644 components/misc/pathgridutils.hpp diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index a769d85cdd..7f6003d5b2 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -1,13 +1,11 @@ #include "aicombat.hpp" -#include -#include - -#include - -#include - #include +#include +#include +#include +#include +#include #include #include "../mwphysics/raycasting.hpp" @@ -393,8 +391,8 @@ namespace MWMechanics osg::Vec3f localPos = actor.getRefData().getPosition().asVec3(); coords.toLocal(localPos); - size_t closestPointIndex = PathFinder::getClosestPoint(pathgrid, localPos); - for (size_t i = 0; i < pathgrid->mPoints.size(); i++) + const std::size_t closestPointIndex = Misc::getClosestPoint(*pathgrid, localPos); + for (std::size_t i = 0; i < pathgrid->mPoints.size(); i++) { if (i != closestPointIndex && getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, i)) diff --git a/apps/openmw/mwmechanics/aicombat.hpp b/apps/openmw/mwmechanics/aicombat.hpp index d5a9c3464c..42baaf6349 100644 --- a/apps/openmw/mwmechanics/aicombat.hpp +++ b/apps/openmw/mwmechanics/aicombat.hpp @@ -2,12 +2,13 @@ #define GAME_MWMECHANICS_AICOMBAT_H #include "aitemporarybase.hpp" +#include "aitimer.hpp" +#include "movement.hpp" #include "typedaipackage.hpp" #include "../mwworld/cellstore.hpp" // for Doors -#include "aitimer.hpp" -#include "movement.hpp" +#include namespace ESM { diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index edb62c97c4..42aa62ffe3 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -16,6 +16,8 @@ namespace ESM { struct Cell; + struct Pathgrid; + namespace AiSequence { struct AiSequence; diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index dfa0bc41ac..7ebf37158b 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "../mwbase/environment.hpp" @@ -821,7 +822,7 @@ namespace MWMechanics if (pathgrid == nullptr || pathgrid->mPoints.empty()) return; - const size_t index = PathFinder::getClosestPoint(pathgrid, dest); + const size_t index = Misc::getClosestPoint(*pathgrid, dest); getPathGridGraph(pathgrid).getNeighbouringPoints(index, points); } @@ -854,7 +855,7 @@ namespace MWMechanics const osg::Vec3f npcPos = converter.toLocalVec3(mInitialActorPosition); // Find closest pathgrid point - size_t closestPointIndex = PathFinder::getClosestPoint(pathgrid, npcPos); + const std::size_t closestPointIndex = Misc::getClosestPoint(*pathgrid, npcPos); // mAllowedPositions for this actor with pathgrid point indexes based on mDistance // and if the point is connected to the closest current point diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index 0518015936..3e0b704524 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -1,14 +1,15 @@ #ifndef GAME_MWMECHANICS_AIWANDER_H #define GAME_MWMECHANICS_AIWANDER_H -#include "typedaipackage.hpp" - -#include -#include - #include "aitemporarybase.hpp" #include "aitimer.hpp" #include "pathfinding.hpp" +#include "typedaipackage.hpp" + +#include + +#include +#include namespace ESM { diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index dcad29c907..d8b17529ab 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -38,7 +39,7 @@ namespace // points to a quadtree may help for (size_t counter = 0; counter < grid->mPoints.size(); counter++) { - float potentialDistBetween = MWMechanics::PathFinder::distanceSquared(grid->mPoints[counter], pos); + float potentialDistBetween = Misc::distanceSquared(grid->mPoints[counter], pos); if (potentialDistBetween < closestDistanceReachable) { // found a closer one @@ -197,7 +198,7 @@ namespace MWMechanics // point right behind the wall that is closer than any pathgrid // point outside the wall osg::Vec3f startPointInLocalCoords(converter.toLocalVec3(startPoint)); - size_t startNode = getClosestPoint(pathgrid, startPointInLocalCoords); + const size_t startNode = Misc::getClosestPoint(*pathgrid, startPointInLocalCoords); osg::Vec3f endPointInLocalCoords(converter.toLocalVec3(endPoint)); std::pair endNode @@ -206,8 +207,8 @@ namespace MWMechanics // if it's shorter for actor to travel from start to end, than to travel from either // start or end to nearest pathgrid point, just travel from start to end. float startToEndLength2 = (endPointInLocalCoords - startPointInLocalCoords).length2(); - float endTolastNodeLength2 = distanceSquared(pathgrid->mPoints[endNode.first], endPointInLocalCoords); - float startTo1stNodeLength2 = distanceSquared(pathgrid->mPoints[startNode], startPointInLocalCoords); + float endTolastNodeLength2 = Misc::distanceSquared(pathgrid->mPoints[endNode.first], endPointInLocalCoords); + float startTo1stNodeLength2 = Misc::distanceSquared(pathgrid->mPoints[startNode], startPointInLocalCoords); if ((startToEndLength2 < startTo1stNodeLength2) || (startToEndLength2 < endTolastNodeLength2)) { *out++ = endPoint; diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index 20ba85cc96..45827a25df 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -5,12 +5,11 @@ #include #include +#include + #include #include #include -#include -#include -#include namespace MWWorld { @@ -146,43 +145,6 @@ namespace MWMechanics mPath.push_back(point); } - // Slightly cheaper version for comparisons. - // Caller needs to be careful for very short distances (i.e. less than 1) - // or when accumuating the results i.e. (a + b)^2 != a^2 + b^2 - // - static float distanceSquared(const ESM::Pathgrid::Point& point, const osg::Vec3f& pos) - { - return (Misc::Convert::makeOsgVec3f(point) - pos).length2(); - } - - // Return the closest pathgrid point index from the specified position - // coordinates. NOTE: Does not check if there is a sensible way to get there - // (e.g. a cliff in front). - // - // NOTE: pos is expected to be in local coordinates, as is grid->mPoints - // - static size_t getClosestPoint(const ESM::Pathgrid* grid, const osg::Vec3f& pos) - { - assert(grid && !grid->mPoints.empty()); - - float distanceBetween = distanceSquared(grid->mPoints[0], pos); - size_t closestIndex = 0; - - // TODO: if this full scan causes performance problems mapping pathgrid - // points to a quadtree may help - for (size_t counter = 1; counter < grid->mPoints.size(); counter++) - { - float potentialDistBetween = distanceSquared(grid->mPoints[counter], pos); - if (potentialDistBetween < distanceBetween) - { - distanceBetween = potentialDistBetween; - closestIndex = counter; - } - } - - return closestIndex; - } - private: bool mConstructed = false; std::deque mPath; diff --git a/components/misc/pathgridutils.hpp b/components/misc/pathgridutils.hpp new file mode 100644 index 0000000000..5ca58f4d08 --- /dev/null +++ b/components/misc/pathgridutils.hpp @@ -0,0 +1,53 @@ +#ifndef OPENMW_COMPONENTS_MISC_PATHGRIDUTILS_H +#define OPENMW_COMPONENTS_MISC_PATHGRIDUTILS_H + +#include "convert.hpp" + +#include + +#include + +#include + +namespace Misc +{ + // Slightly cheaper version for comparisons. + // Caller needs to be careful for very short distances (i.e. less than 1) + // or when accumuating the results i.e. (a + b)^2 != a^2 + b^2 + // + inline float distanceSquared(const ESM::Pathgrid::Point& point, const osg::Vec3f& pos) + { + return (Misc::Convert::makeOsgVec3f(point) - pos).length2(); + } + + // Return the closest pathgrid point index from the specified position + // coordinates. NOTE: Does not check if there is a sensible way to get there + // (e.g. a cliff in front). + // + // NOTE: pos is expected to be in local coordinates, as is grid->mPoints + // + inline std::size_t getClosestPoint(const ESM::Pathgrid& grid, const osg::Vec3f& pos) + { + if (grid.mPoints.empty()) + throw std::invalid_argument("Pathgrid has no points"); + + float minDistance = distanceSquared(grid.mPoints[0], pos); + std::size_t closestIndex = 0; + + // TODO: if this full scan causes performance problems mapping pathgrid + // points to a quadtree may help + for (std::size_t i = 1; i < grid.mPoints.size(); ++i) + { + const float distance = distanceSquared(grid.mPoints[i], pos); + if (minDistance > distance) + { + minDistance = distance; + closestIndex = i; + } + } + + return closestIndex; + } +} + +#endif From 5dfda0090b7565bc0abf763b112dc03f57202fe1 Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 12 Apr 2025 13:35:39 +0200 Subject: [PATCH 15/23] Remove redundant cell argument from build path functions Actor can provide a cell. --- apps/openmw/mwmechanics/aicombat.cpp | 8 ++++---- apps/openmw/mwmechanics/aipackage.cpp | 4 ++-- apps/openmw/mwmechanics/aiwander.cpp | 4 ++-- apps/openmw/mwmechanics/pathfinding.cpp | 20 ++++++++++---------- apps/openmw/mwmechanics/pathfinding.hpp | 12 ++++++------ 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 7f6003d5b2..295c2bb436 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -300,8 +300,8 @@ namespace MWMechanics const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); const ESM::Pathgrid* pathgrid = world->getStore().get().search(*actor.getCell()->getCell()); const auto& pathGridGraph = getPathGridGraph(pathgrid); - mPathFinder.buildPath(actor, vActorPos, vTargetPos, actor.getCell(), pathGridGraph, agentBounds, - navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full); + mPathFinder.buildPath(actor, vActorPos, vTargetPos, pathGridGraph, agentBounds, navigatorFlags, areaCosts, + storage.mAttackRange, PathType::Full); if (!mPathFinder.isPathConstructed()) { @@ -314,8 +314,8 @@ namespace MWMechanics if (hit.has_value() && (*hit - vTargetPos).length() <= rangeAttack) { // If the point is close enough, try to find a path to that point. - mPathFinder.buildPath(actor, vActorPos, *hit, actor.getCell(), pathGridGraph, agentBounds, - navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full); + mPathFinder.buildPath(actor, vActorPos, *hit, pathGridGraph, agentBounds, navigatorFlags, areaCosts, + storage.mAttackRange, PathType::Full); if (mPathFinder.isPathConstructed()) { // If path to that point is found use it as custom destination. diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 4bcfc7dedd..b71c01397b 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -180,8 +180,8 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& = world->getStore().get().search(*actor.getCell()->getCell()); const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); - mPathFinder.buildLimitedPath(actor, position, dest, actor.getCell(), getPathGridGraph(pathgrid), - agentBounds, navigatorFlags, areaCosts, endTolerance, pathType); + mPathFinder.buildLimitedPath(actor, position, dest, getPathGridGraph(pathgrid), agentBounds, + navigatorFlags, areaCosts, endTolerance, pathType); mRotateOnTheRunChecks = 3; // give priority to go directly on target if there is minimal opportunity diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 7ebf37158b..40b8ee1a5a 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -253,8 +253,8 @@ namespace MWMechanics constexpr float endTolerance = 0; const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); - mPathFinder.buildPath(actor, pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(pathgrid), - agentBounds, navigatorFlags, areaCosts, endTolerance, PathType::Full); + mPathFinder.buildPath(actor, pos.asVec3(), mDestination, getPathGridGraph(pathgrid), agentBounds, + navigatorFlags, areaCosts, endTolerance, PathType::Full); } if (mPathFinder.isPathConstructed()) diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index d8b17529ab..8a3d9116e0 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -391,12 +391,12 @@ namespace MWMechanics } void PathFinder::buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, - const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) + const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType) { mPath.clear(); - mCell = cell; + mCell = actor.getCell(); DetourNavigator::Status status = DetourNavigator::Status::NavMeshNotFound; @@ -452,9 +452,9 @@ namespace MWMechanics } void PathFinder::buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, - const osg::Vec3f& endPoint, const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, - const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) + const osg::Vec3f& endPoint, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType) { const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); const auto maxDistance @@ -462,9 +462,9 @@ namespace MWMechanics const auto startToEnd = endPoint - startPoint; const auto distance = startToEnd.length(); if (distance <= maxDistance) - return buildPath(actor, startPoint, endPoint, cell, pathgridGraph, agentBounds, flags, areaCosts, - endTolerance, pathType); + return buildPath( + actor, startPoint, endPoint, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType); const auto end = startPoint + startToEnd * maxDistance / distance; - buildPath(actor, startPoint, end, cell, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType); + buildPath(actor, startPoint, end, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType); } } diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index 45827a25df..77b2f83c12 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -111,14 +111,14 @@ namespace MWMechanics PathType pathType); void buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, - const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType); + const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType); void buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, - const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType); + const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType); /// Remove front point if exist and within tolerance void update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance, From 4433e3c2de68cee080452b0548fd2a39c9963229 Mon Sep 17 00:00:00 2001 From: elsid Date: Mon, 21 Apr 2025 16:03:10 +0200 Subject: [PATCH 16/23] Build path over navmesh for wandering actors Using a path over pathgrid as checkpoints. This allows to avoid having paths going through obstacles if they are placed over pathgrid points. --- .../detournavigator/navigator.cpp | 186 +++++++++++++++--- apps/openmw/mwlua/nearbybindings.cpp | 18 +- apps/openmw/mwmechanics/aipackage.cpp | 6 +- apps/openmw/mwmechanics/aiwander.cpp | 90 +++++---- apps/openmw/mwmechanics/pathfinding.cpp | 33 ++-- apps/openmw/mwmechanics/pathfinding.hpp | 11 +- components/detournavigator/findsmoothpath.hpp | 84 ++++++-- components/detournavigator/navigatorutils.hpp | 10 +- files/lua_api/openmw/nearby.lua | 1 + .../integration_tests/test_lua_api/global.lua | 1 + .../integration_tests/test_lua_api/menu.lua | 1 + .../integration_tests/test_lua_api/player.lua | 33 ++++ 12 files changed, 366 insertions(+), 108 deletions(-) diff --git a/apps/components_tests/detournavigator/navigator.cpp b/apps/components_tests/detournavigator/navigator.cpp index 0a78c33c9e..a6e1d26c79 100644 --- a/apps/components_tests/detournavigator/navigator.cpp +++ b/apps/components_tests/detournavigator/navigator.cpp @@ -139,7 +139,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, find_path_for_empty_should_return_empty) { - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::NavMeshNotFound); EXPECT_EQ(mPath, std::deque()); } @@ -147,7 +147,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, find_path_for_existing_agent_with_no_navmesh_should_throw_exception) { ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::StartPolygonNotFound); } @@ -156,7 +156,7 @@ namespace ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->removeAgent(mAgentBounds); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::StartPolygonNotFound); } @@ -172,7 +172,7 @@ namespace updateGuard.reset(); mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -194,7 +194,7 @@ namespace updateGuard.reset(); mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mStart, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mStart, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre(Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125))) << mPath; @@ -218,7 +218,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -237,7 +237,7 @@ namespace mPath.clear(); mOut = std::back_inserter(mPath); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -265,7 +265,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -285,7 +285,7 @@ namespace mPath.clear(); mOut = std::back_inserter(mPath); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -318,7 +318,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -386,7 +386,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -421,7 +421,7 @@ namespace mEnd.x() = 256; mEnd.z() = 300; - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -453,8 +453,8 @@ namespace mStart.x() = 256; mEnd.x() = 256; - EXPECT_EQ( - findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, + {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -487,8 +487,8 @@ namespace mStart.x() = 256; mEnd.x() = 256; - EXPECT_EQ( - findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, + {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -520,7 +520,7 @@ namespace mStart.x() = 256; mEnd.x() = 256; - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -549,7 +549,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -577,7 +577,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -658,7 +658,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -781,7 +781,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -806,7 +806,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::PartialPath); EXPECT_THAT(mPath, @@ -834,7 +834,7 @@ namespace const float endTolerance = 1000.0f; - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, endTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, endTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -979,6 +979,146 @@ namespace EXPECT_EQ(usedNavMeshTiles, 854); } + TEST_F(DetourNavigatorNavigatorTest, find_path_should_return_path_around_steep_mountains) + { + const std::array heightfieldData{ { + 0, 0, 0, 0, 0, // row 0 + 0, 0, 0, 0, 0, // row 1 + 0, 0, 1000, 0, 0, // row 2 + 0, 0, 0, 0, 0, // row 3 + 0, 0, 0, 0, 0, // row 4 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const osg::Vec3f start(56, 56, 12); + const osg::Vec3f end(464, 464, 12); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 56.66664886474609375, 11.33333301544189453125), + Vec3fEq(396.666656494140625, 79.33331298828125, 11.33333301544189453125), + Vec3fEq(430.666656494140625, 113.33331298828125, 11.33333301544189453125), + Vec3fEq(463.999969482421875, 463.999969482421875, 11.33333301544189453125))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, find_path_should_return_path_around_steep_cliffs) + { + const std::array heightfieldData{ { + 0, 0, 0, 0, 0, // row 0 + 0, 0, 0, 0, 0, // row 1 + 0, 0, -1000, 0, 0, // row 2 + 0, 0, 0, 0, 0, // row 3 + 0, 0, 0, 0, 0, // row 4 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const osg::Vec3f start(56, 56, 12); + const osg::Vec3f end(464, 464, 12); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 56.66664886474609375, 8.66659259796142578125), + Vec3fEq(385.33331298828125, 79.33331298828125, 8.66659259796142578125), + Vec3fEq(430.666656494140625, 124.66664886474609375, 8.66659259796142578125), + Vec3fEq(463.999969482421875, 463.999969482421875, 8.66659259796142578125))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, find_path_should_return_path_with_checkpoints) + { + const std::array heightfieldData{ { + 0, 0, 0, 0, 0, // row 0 + 0, 0, 0, 0, 0, // row 1 + 0, 0, 1000, 0, 0, // row 2 + 0, 0, 0, 0, 0, // row 3 + 0, 0, 0, 0, 0, // row 4 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const std::vector checkpoints = { + osg::Vec3f(400, 70, 12), + }; + + const osg::Vec3f start(56, 56, 12); + const osg::Vec3f end(464, 464, 12); + + EXPECT_EQ( + findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, checkpoints, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 56.66664886474609375, 11.33333301544189453125), + Vec3fEq(400, 70, 11.33333301544189453125), + Vec3fEq(430.666656494140625, 113.33331298828125, 11.33333301544189453125), + Vec3fEq(463.999969482421875, 463.999969482421875, 11.33333301544189453125))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, find_path_should_skip_unreachable_checkpoints) + { + const std::array heightfieldData{ { + 0, 0, 0, 0, 0, // row 0 + 0, 0, 0, 0, 0, // row 1 + 0, 0, 1000, 0, 0, // row 2 + 0, 0, 0, 0, 0, // row 3 + 0, 0, 0, 0, 0, // row 4 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const std::vector checkpoints = { + osg::Vec3f(400, 70, 10000), + osg::Vec3f(256, 256, 1000), + osg::Vec3f(-1000, -1000, 0), + }; + + const osg::Vec3f start(56, 56, 12); + const osg::Vec3f end(464, 464, 12); + + EXPECT_EQ( + findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, checkpoints, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 56.66664886474609375, 11.33333301544189453125), + Vec3fEq(396.666656494140625, 79.33331298828125, 11.33333301544189453125), + Vec3fEq(430.666656494140625, 113.33331298828125, 11.33333301544189453125), + Vec3fEq(463.999969482421875, 463.999969482421875, 11.33333301544189453125))) + << mPath; + } + struct DetourNavigatorNavigatorNotSupportedAgentBoundsTest : TestWithParam { }; diff --git a/apps/openmw/mwlua/nearbybindings.cpp b/apps/openmw/mwlua/nearbybindings.cpp index a6d762499a..6c244a0fd4 100644 --- a/apps/openmw/mwlua/nearbybindings.cpp +++ b/apps/openmw/mwlua/nearbybindings.cpp @@ -233,6 +233,7 @@ namespace MWLua DetourNavigator::Flags includeFlags = defaultIncludeFlags; DetourNavigator::AreaCosts areaCosts{}; float destinationTolerance = 1; + std::vector checkpoints; if (options.has_value()) { @@ -258,13 +259,24 @@ namespace MWLua } if (const auto& v = options->get>("destinationTolerance")) destinationTolerance = *v; + if (const auto& t = options->get>("checkpoints")) + { + for (const auto& [k, v] : *t) + { + const int index = k.as(); + const osg::Vec3f position = v.as(); + if (index != static_cast(checkpoints.size() + 1)) + throw std::runtime_error("checkpoints is not an array"); + checkpoints.push_back(position); + } + } } std::vector path; - const DetourNavigator::Status status - = DetourNavigator::findPath(*MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, - source, destination, includeFlags, areaCosts, destinationTolerance, std::back_inserter(path)); + const DetourNavigator::Status status = DetourNavigator::findPath( + *MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, source, destination, + includeFlags, areaCosts, destinationTolerance, checkpoints, std::back_inserter(path)); sol::table result(lua, sol::create); LuaUtil::copyVectorToTable(path, result); diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index b71c01397b..3fcb28307c 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -501,7 +501,11 @@ DetourNavigator::Flags MWMechanics::AiPackage::getNavigatorFlags(const MWWorld:: result |= DetourNavigator::Flag_swim; if (actorClass.canWalk(actor) && actor.getClass().getWalkSpeed(actor) > 0) - result |= DetourNavigator::Flag_walk | DetourNavigator::Flag_usePathgrid; + { + result |= DetourNavigator::Flag_walk; + if (getTypeId() != AiPackageTypeId::Wander) + result |= DetourNavigator::Flag_usePathgrid; + } if (canOpenDoors(actor) && getTypeId() != AiPackageTypeId::Wander) result |= DetourNavigator::Flag_openDoor; diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 40b8ee1a5a..71cc8b5e7d 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -242,20 +242,12 @@ namespace MWMechanics { const ESM::Pathgrid* pathgrid = MWBase::Environment::get().getESMStore()->get().search(*actor.getCell()->getCell()); - if (mUsePathgrid) - { - mPathFinder.buildPathByPathgrid( - pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(pathgrid)); - } - else - { - const auto agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor); - constexpr float endTolerance = 0; - const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); - const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); - mPathFinder.buildPath(actor, pos.asVec3(), mDestination, getPathGridGraph(pathgrid), agentBounds, - navigatorFlags, areaCosts, endTolerance, PathType::Full); - } + const auto agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor); + constexpr float endTolerance = 0; + const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); + const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); + mPathFinder.buildPath(actor, pos.asVec3(), mDestination, getPathGridGraph(pathgrid), agentBounds, + navigatorFlags, areaCosts, endTolerance, PathType::Full); if (mPathFinder.isPathConstructed()) storage.setState(AiWanderStorage::Wander_Walking, !mUsePathgrid); @@ -633,38 +625,64 @@ namespace MWMechanics void AiWander::setPathToAnAllowedPosition( const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos) { - auto world = MWBase::Environment::get().getWorld(); - auto& prng = world->getPrng(); + MWBase::World& world = *MWBase::Environment::get().getWorld(); + Misc::Rng::Generator& prng = world.getPrng(); const std::size_t randomAllowedPositionIndex = static_cast(Misc::Rng::rollDice(storage.mAllowedPositions.size(), prng)); const osg::Vec3f randomAllowedPosition = storage.mAllowedPositions[randomAllowedPositionIndex]; const osg::Vec3f start = actorPos.asVec3(); - // don't take shortcuts for wandering - const ESM::Pathgrid* pathgrid = world->getStore().get().search(*actor.getCell()->getCell()); - mPathFinder.buildPathByPathgrid(start, randomAllowedPosition, actor.getCell(), getPathGridGraph(pathgrid)); + const MWWorld::Cell& cell = *actor.getCell()->getCell(); + const ESM::Pathgrid* pathgrid = world.getStore().get().search(cell); + const PathgridGraph& pathgridGraph = getPathGridGraph(pathgrid); - if (mPathFinder.isPathConstructed()) - { - mDestination = randomAllowedPosition; - mHasDestination = true; - mUsePathgrid = true; - // Remove this position as an option and add back the previously used position (stops NPC from picking the - // same position): - storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex); - // check if mCurrentPosition was taken out of mAllowedPositions - if (storage.mTrimCurrentPosition && storage.mAllowedPositions.size() > 1) - storage.mTrimCurrentPosition = false; - else - storage.mAllowedPositions.push_back(storage.mCurrentPosition); - storage.mCurrentPosition = randomAllowedPosition; + const Misc::CoordinateConverter converter = Misc::makeCoordinateConverter(cell); + std::deque path + = pathgridGraph.aStarSearch(Misc::getClosestPoint(*pathgrid, converter.toLocalVec3(start)), + Misc::getClosestPoint(*pathgrid, converter.toLocalVec3(randomAllowedPosition))); - storage.setState(AiWanderStorage::Wander_Walking); - } // Choose a different position and delete this one from possible positions because it is uncreachable: - else + if (path.empty()) + { storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex); + return; + } + + // Drop nearest pathgrid point. + path.pop_front(); + + std::vector checkpoints(path.size()); + for (std::size_t i = 0; i < path.size(); ++i) + checkpoints[i] = Misc::Convert::makeOsgVec3f(converter.toWorldPoint(path[i])); + + const DetourNavigator::AgentBounds agentBounds = world.getPathfindingAgentBounds(actor); + const DetourNavigator::Flags flags = getNavigatorFlags(actor); + const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, flags); + constexpr float endTolerance = 0; + mPathFinder.buildPath(actor, start, randomAllowedPosition, pathgridGraph, agentBounds, flags, areaCosts, + endTolerance, PathType::Full, checkpoints); + + if (!mPathFinder.isPathConstructed()) + { + storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex); + return; + } + + mDestination = randomAllowedPosition; + mHasDestination = true; + mUsePathgrid = true; + // Remove this position as an option and add back the previously used position (stops NPC from picking the + // same position): + storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex); + // check if mCurrentPosition was taken out of mAllowedPositions + if (storage.mTrimCurrentPosition && storage.mAllowedPositions.size() > 1) + storage.mTrimCurrentPosition = false; + else + storage.mAllowedPositions.push_back(storage.mCurrentPosition); + storage.mCurrentPosition = randomAllowedPosition; + + storage.setState(AiWanderStorage::Wander_Walking); } void AiWander::stopWalking(const MWWorld::Ptr& actor) diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index 8a3d9116e0..165250c5c8 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -360,26 +360,16 @@ namespace MWMechanics mConstructed = true; } - void PathFinder::buildPathByPathgrid(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph) - { - mPath.clear(); - mCell = cell; - - buildPathByPathgridImpl(startPoint, endPoint, pathgridGraph, std::back_inserter(mPath)); - - mConstructed = !mPath.empty(); - } - void PathFinder::buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) + const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType, + std::span checkpoints) { mPath.clear(); // If it's not possible to build path over navmesh due to disabled navmesh generation fallback to straight path DetourNavigator::Status status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, flags, - areaCosts, endTolerance, pathType, std::back_inserter(mPath)); + areaCosts, endTolerance, pathType, checkpoints, std::back_inserter(mPath)); if (status != DetourNavigator::Status::Success) mPath.clear(); @@ -393,7 +383,7 @@ namespace MWMechanics void PathFinder::buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, - PathType pathType) + PathType pathType, std::span checkpoints) { mPath.clear(); mCell = actor.getCell(); @@ -403,7 +393,7 @@ namespace MWMechanics if (!actor.getClass().isPureWaterCreature(actor) && !actor.getClass().isPureFlyingCreature(actor)) { status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, flags, areaCosts, endTolerance, - pathType, std::back_inserter(mPath)); + pathType, checkpoints, std::back_inserter(mPath)); if (status != DetourNavigator::Status::Success) mPath.clear(); } @@ -412,7 +402,7 @@ namespace MWMechanics && (flags & DetourNavigator::Flag_usePathgrid) == 0) { status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, - flags | DetourNavigator::Flag_usePathgrid, areaCosts, endTolerance, pathType, + flags | DetourNavigator::Flag_usePathgrid, areaCosts, endTolerance, pathType, checkpoints, std::back_inserter(mPath)); if (status != DetourNavigator::Status::Success) mPath.clear(); @@ -430,12 +420,13 @@ namespace MWMechanics DetourNavigator::Status PathFinder::buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, - PathType pathType, std::back_insert_iterator> out) + PathType pathType, std::span checkpoints, + std::back_insert_iterator> out) { - const auto world = MWBase::Environment::get().getWorld(); - const auto navigator = world->getNavigator(); - const auto status = DetourNavigator::findPath( - *navigator, agentBounds, startPoint, endPoint, flags, areaCosts, endTolerance, out); + const MWBase::World& world = *MWBase::Environment::get().getWorld(); + const DetourNavigator::Navigator& navigator = *world.getNavigator(); + const DetourNavigator::Status status = DetourNavigator::findPath( + navigator, agentBounds, startPoint, endPoint, flags, areaCosts, endTolerance, checkpoints, out); if (pathType == PathType::Partial && status == DetourNavigator::Status::PartialPath) return DetourNavigator::Status::Success; diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index 77b2f83c12..55064d9e88 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include @@ -102,18 +103,15 @@ namespace MWMechanics void buildStraightPath(const osg::Vec3f& endPoint); - void buildPathByPathgrid(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph); - void buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, - PathType pathType); + PathType pathType, std::span checkpoints = {}); void buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, - PathType pathType); + PathType pathType, std::span checkpoints = {}); void buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, @@ -156,7 +154,8 @@ namespace MWMechanics [[nodiscard]] DetourNavigator::Status buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, - PathType pathType, std::back_insert_iterator> out); + PathType pathType, std::span checkpoints, + std::back_insert_iterator> out); }; } diff --git a/components/detournavigator/findsmoothpath.hpp b/components/detournavigator/findsmoothpath.hpp index e5efa8815f..d01b6bc1c7 100644 --- a/components/detournavigator/findsmoothpath.hpp +++ b/components/detournavigator/findsmoothpath.hpp @@ -13,7 +13,6 @@ #include -#include #include #include #include @@ -64,6 +63,25 @@ namespace DetourNavigator std::reference_wrapper mSettings; }; + template + class ToNavMeshCoordinatesSpan + { + public: + explicit ToNavMeshCoordinatesSpan(std::span span, const RecastSettings& settings) + : mSpan(span) + , mSettings(settings) + { + } + + std::size_t size() const noexcept { return mSpan.size(); } + + T operator[](std::size_t i) const noexcept { return toNavMeshCoordinates(mSettings, mSpan[i]); } + + private: + std::span mSpan; + const RecastSettings& mSettings; + }; + inline std::optional findPolygonPath(const dtNavMeshQuery& navMeshQuery, const dtPolyRef startRef, const dtPolyRef endRef, const osg::Vec3f& startPos, const osg::Vec3f& endPos, const dtQueryFilter& queryFilter, std::span pathBuffer) @@ -79,7 +97,7 @@ namespace DetourNavigator } Status makeSmoothPath(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& start, const osg::Vec3f& end, - std::span polygonPath, std::size_t polygonPathSize, std::size_t maxSmoothPathSize, + std::span polygonPath, std::size_t polygonPathSize, std::size_t maxSmoothPathSize, bool skipFirst, std::output_iterator auto& out) { assert(polygonPathSize <= polygonPath.size()); @@ -95,7 +113,7 @@ namespace DetourNavigator dtStatusFailed(status)) return Status::FindStraightPathFailed; - for (int i = 0; i < cornersCount; ++i) + for (int i = skipFirst ? 1 : 0; i < cornersCount; ++i) *out++ = Misc::Convert::makeOsgVec3f(&cornerVertsBuffer[static_cast(i) * 3]); return Status::Success; @@ -103,7 +121,8 @@ namespace DetourNavigator Status findSmoothPath(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, const DetourSettings& settings, - float endTolerance, std::output_iterator auto out) + float endTolerance, const ToNavMeshCoordinatesSpan& checkpoints, + std::output_iterator auto out) { dtQueryFilter queryFilter; queryFilter.setIncludeFlags(includeFlags); @@ -131,29 +150,66 @@ namespace DetourNavigator return Status::EndPolygonNotFound; std::vector polygonPath(settings.mMaxPolygonPathSize); - const auto polygonPathSize - = findPolygonPath(navMeshQuery, startRef, endRef, startNavMeshPos, endNavMeshPos, queryFilter, polygonPath); + std::span polygonPathBuffer = polygonPath; + dtPolyRef currentRef = startRef; + osg::Vec3f currentNavMeshPos = startNavMeshPos; + bool skipFirst = false; - if (!polygonPathSize.has_value()) + for (std::size_t i = 0; i < checkpoints.size(); ++i) + { + const osg::Vec3f checkpointPos = checkpoints[i]; + osg::Vec3f checkpointNavMeshPos; + dtPolyRef checkpointRef; + if (const dtStatus status = navMeshQuery.findNearestPoly(checkpointPos.ptr(), polyHalfExtents.ptr(), + &queryFilter, &checkpointRef, checkpointNavMeshPos.ptr()); + dtStatusFailed(status) || checkpointRef == 0) + continue; + + const std::optional toCheckpointPathSize = findPolygonPath(navMeshQuery, currentRef, + checkpointRef, currentNavMeshPos, checkpointNavMeshPos, queryFilter, polygonPath); + + if (!toCheckpointPathSize.has_value()) + continue; + + if (*toCheckpointPathSize == 0) + continue; + + if (polygonPath[*toCheckpointPathSize - 1] != checkpointRef) + continue; + + const Status smoothStatus = makeSmoothPath(navMeshQuery, currentNavMeshPos, checkpointNavMeshPos, + polygonPath, *toCheckpointPathSize, settings.mMaxSmoothPathSize, skipFirst, out); + + if (smoothStatus != Status::Success) + return smoothStatus; + + currentRef = checkpointRef; + currentNavMeshPos = checkpointNavMeshPos; + skipFirst = true; + } + + const std::optional toEndPathSize = findPolygonPath( + navMeshQuery, currentRef, endRef, currentNavMeshPos, endNavMeshPos, queryFilter, polygonPathBuffer); + + if (!toEndPathSize.has_value()) return Status::FindPathOverPolygonsFailed; - if (*polygonPathSize == 0) - return Status::Success; + if (*toEndPathSize == 0) + return currentRef == endRef ? Status::Success : Status::PartialPath; osg::Vec3f targetNavMeshPos; if (const dtStatus status = navMeshQuery.closestPointOnPoly( - polygonPath[*polygonPathSize - 1], end.ptr(), targetNavMeshPos.ptr(), nullptr); + polygonPath[*toEndPathSize - 1], end.ptr(), targetNavMeshPos.ptr(), nullptr); dtStatusFailed(status)) return Status::TargetPolygonNotFound; - const bool partialPath = polygonPath[*polygonPathSize - 1] != endRef; - const Status smoothStatus = makeSmoothPath(navMeshQuery, startNavMeshPos, targetNavMeshPos, polygonPath, - *polygonPathSize, settings.mMaxSmoothPathSize, out); + const Status smoothStatus = makeSmoothPath(navMeshQuery, currentNavMeshPos, targetNavMeshPos, polygonPath, + *toEndPathSize, settings.mMaxSmoothPathSize, skipFirst, out); if (smoothStatus != Status::Success) return smoothStatus; - return partialPath ? Status::PartialPath : Status::Success; + return polygonPath[*toEndPathSize - 1] == endRef ? Status::Success : Status::PartialPath; } } diff --git a/components/detournavigator/navigatorutils.hpp b/components/detournavigator/navigatorutils.hpp index ca02682ecd..d3b8b5e35a 100644 --- a/components/detournavigator/navigatorutils.hpp +++ b/components/detournavigator/navigatorutils.hpp @@ -11,6 +11,7 @@ #include #include +#include namespace DetourNavigator { @@ -21,13 +22,13 @@ namespace DetourNavigator * @param end path at given point. * @param includeFlags setup allowed navmesh areas. * @param out the beginning of the destination range. - * @param endTolerance defines maximum allowed distance to end path point in addition to agentHalfExtents + * @param endTolerance defines maximum allowed distance to end path point in addition to agentHalfExtents. + * @param checkpoints is a sequence of positions the path should go over if possible. * @return Status. - * Equal to out if no path is found. */ inline Status findPath(const Navigator& navigator, const AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, float endTolerance, - std::output_iterator auto out) + std::span checkpoints, std::output_iterator auto out) { const auto navMesh = navigator.getNavMesh(agentBounds); if (navMesh == nullptr) @@ -37,7 +38,8 @@ namespace DetourNavigator const auto locked = navMesh->lock(); return findSmoothPath(locked->getQuery(), toNavMeshCoordinates(settings.mRecast, agentBounds.mHalfExtents), toNavMeshCoordinates(settings.mRecast, start), toNavMeshCoordinates(settings.mRecast, end), includeFlags, - areaCosts, settings.mDetour, endTolerance, outTransform); + areaCosts, settings.mDetour, endTolerance, ToNavMeshCoordinatesSpan(checkpoints, settings.mRecast), + outTransform); } /** diff --git a/files/lua_api/openmw/nearby.lua b/files/lua_api/openmw/nearby.lua index 29f1e79c24..a09e0cf50a 100644 --- a/files/lua_api/openmw/nearby.lua +++ b/files/lua_api/openmw/nearby.lua @@ -180,6 +180,7 @@ -- @field [parent=#FindPathOptions] #AreaCosts areaCosts a table defining relative cost for each type of area. -- @field [parent=#FindPathOptions] #number destinationTolerance a floating point number representing maximum allowed -- distance between destination and a nearest point on the navigation mesh in addition to agent size (default: 1). +-- @field [parent=#FindPathOptions] #table checkpoints an array of positions to build path over if possible. --- -- A table of parameters for @{#nearby.findRandomPointAroundCircle} and @{#nearby.castNavigationRay} diff --git a/scripts/data/integration_tests/test_lua_api/global.lua b/scripts/data/integration_tests/test_lua_api/global.lua index e5bfb5ef36..3a85e224d1 100644 --- a/scripts/data/integration_tests/test_lua_api/global.lua +++ b/scripts/data/integration_tests/test_lua_api/global.lua @@ -315,6 +315,7 @@ registerPlayerTest('player rotation') registerPlayerTest('player forward running') registerPlayerTest('player diagonal walking') registerPlayerTest('findPath') +registerPlayerTest('findPath with checkpoints') registerPlayerTest('findRandomPointAroundCircle') registerPlayerTest('castNavigationRay') registerPlayerTest('findNearestNavMeshPosition') diff --git a/scripts/data/integration_tests/test_lua_api/menu.lua b/scripts/data/integration_tests/test_lua_api/menu.lua index 3fc45bf2e7..8a2a278046 100644 --- a/scripts/data/integration_tests/test_lua_api/menu.lua +++ b/scripts/data/integration_tests/test_lua_api/menu.lua @@ -82,6 +82,7 @@ registerGlobalTest('player rotation', 'rotating player should not lead to nan ro registerGlobalTest('player forward running') registerGlobalTest('player diagonal walking') registerGlobalTest('findPath') +registerGlobalTest('findPath with checkpoints') registerGlobalTest('findRandomPointAroundCircle') registerGlobalTest('castNavigationRay') registerGlobalTest('findNearestNavMeshPosition') diff --git a/scripts/data/integration_tests/test_lua_api/player.lua b/scripts/data/integration_tests/test_lua_api/player.lua index 0b481fba2b..88c4d2ee0f 100644 --- a/scripts/data/integration_tests/test_lua_api/player.lua +++ b/scripts/data/integration_tests/test_lua_api/player.lua @@ -179,6 +179,39 @@ testing.registerLocalTest('findPath', testing.expectEqual(status, nearby.FIND_PATH_STATUS.Success, 'Status') testing.expectLessOrEqual((path[#path] - dst):length(), 1, 'Last path point ' .. testing.formatActualExpected(path[#path], dst)) + testing.expectThat(path, matchers.equalTo({ + matchers.closeToVector(util.vector3(4096, 4096, 1746.27099609375), 1e-1), + matchers.closeToVector(util.vector3(4500, 4500, 1745.95263671875), 1e-1), + })) + end) + +testing.registerLocalTest('findPath with checkpoints', + function() + local src = util.vector3(4096, 4096, 1745) + local dst = util.vector3(4500, 4500, 1745.95263671875) + local options = { + agentBounds = types.Actor.getPathfindingAgentBounds(self), + includeFlags = nearby.NAVIGATOR_FLAGS.Walk + nearby.NAVIGATOR_FLAGS.Swim, + areaCosts = { + water = 1, + door = 2, + ground = 1, + pathgrid = 1, + }, + destinationTolerance = 1, + checkpoints = { + util.vector3(4200, 4100, 1750), + }, + } + local status, path = nearby.findPath(src, dst, options) + testing.expectEqual(status, nearby.FIND_PATH_STATUS.Success, 'Status') + testing.expectLessOrEqual((path[#path] - dst):length(), 1, + 'Last path point ' .. testing.formatActualExpected(path[#path], dst)) + testing.expectThat(path, matchers.equalTo({ + matchers.closeToVector(util.vector3(4096, 4096, 1746.27099609375), 1e-1), + matchers.closeToVector(util.vector3(4200, 4100, 1749.5076904296875), 1e-1), + matchers.closeToVector(util.vector3(4500, 4500, 1745.95263671875), 1e-1), + })) end) testing.registerLocalTest('findRandomPointAroundCircle', From c5d74818eb485a84a55c77fc78ca2a782c985ab8 Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 20 Jul 2025 11:09:20 +0200 Subject: [PATCH 17/23] Disable portability-template-virtual-member-function clang tidy warning There is no compatibility problem in practice. /home/elsid/dev/openmw/components/settings/sanitizer.hpp:11:19: error: unspecified virtual member function instantiation; the virtual member function is not instantiated but it might be with a different compiler [portability-template-virtual-member-function,-warnings-as-errors] 11 | virtual T apply(const T& value) const = 0; | ^ /home/elsid/dev/openmw/components/settings/sanitizerimpl.cpp:20:28: note: template instantiated here 20 | struct Max final : Sanitizer | ^ --- .clang-tidy | 1 + 1 file changed, 1 insertion(+) diff --git a/.clang-tidy b/.clang-tidy index 90c72765ca..8597ea5c13 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,6 +1,7 @@ Checks: > -*, portability-*, + -portability-template-virtual-member-function, clang-analyzer-*, -clang-analyzer-optin.*, -clang-analyzer-cplusplus.NewDeleteLeaks, From ec0c76d2f3cbbbc2af082155f46b6f5c6192494d Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 27 Jul 2025 12:11:21 +0200 Subject: [PATCH 18/23] Ignore false positive warning cellSize > 1 so the result of the division cannot be undefined. /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:18:35: error: The result of the '/' expression is undefined [clang-analyzer-core.UndefinedBinaryOperatorResult,-warnings-as-errors] 18 | std::size_t cell = global / (cellSize - 1); | ^ /home/elsid/dev/openmw/components/esmterrain/storage.cpp:244:13: note: Assuming 'lodLevel' is >= 0 244 | if (lodLevel < 0 || 63 < lodLevel) | ^~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/storage.cpp:244:13: note: Left side of '||' is false /home/elsid/dev/openmw/components/esmterrain/storage.cpp:244:29: note: Assuming 'lodLevel' is <= 63 244 | if (lodLevel < 0 || 63 < lodLevel) | ^~~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/storage.cpp:244:9: note: Taking false branch 244 | if (lodLevel < 0 || 63 < lodLevel) | ^ /home/elsid/dev/openmw/components/esmterrain/storage.cpp:247:13: note: Assuming 'size' is > 0 247 | if (size <= 0) | ^~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/storage.cpp:247:9: note: Taking false branch 247 | if (size <= 0) | ^ /home/elsid/dev/openmw/components/esmterrain/storage.cpp:272:13: note: Assuming the condition is false 272 | if (land != nullptr) | ^~~~~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/storage.cpp:272:9: note: Taking false branch 272 | if (land != nullptr) | ^ /home/elsid/dev/openmw/components/esmterrain/storage.cpp:363:9: note: Calling 'sampleCellGrid' 363 | sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, handleSample); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:72:13: note: Assuming 'cellSize' is >= 2 72 | if (cellSize < 2 || !Misc::isPowerOfTwo(cellSize - 1)) | ^~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:72:13: note: Left side of '||' is false /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:72:9: note: Taking false branch 72 | if (cellSize < 2 || !Misc::isPowerOfTwo(cellSize - 1)) | ^ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:75:13: note: Assuming 'sampleSize' is not equal to 0 75 | if (sampleSize == 0 || !Misc::isPowerOfTwo(sampleSize)) | ^~~~~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:75:13: note: Left side of '||' is false /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:75:9: note: Taking false branch 75 | if (sampleSize == 0 || !Misc::isPowerOfTwo(sampleSize)) | ^ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:78:13: note: Assuming 'distance' is >= 2 78 | if (distance < 2 || !Misc::isPowerOfTwo(distance - 1)) | ^~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:78:13: note: Left side of '||' is false /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:78:9: note: Taking false branch 78 | if (distance < 2 || !Misc::isPowerOfTwo(distance - 1)) | ^ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:84:13: note: Assuming 'distance' is >= 'cellSize' 84 | if (distance < cellSize || sampleSize > cellSize - 1) | ^~~~~~~~~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:84:13: note: Left side of '||' is false /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:84:36: note: Assuming the condition is true 84 | if (distance < cellSize || sampleSize > cellSize - 1) | ^~~~~~~~~~~~~~~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:84:9: note: Taking true branch 84 | if (distance < cellSize || sampleSize > cellSize - 1) | ^ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:85:20: note: Calling 'sampleCellGridSimple' 85 | return sampleCellGridSimple(cellSize, sampleSize, beginX, beginY, endX, endY, f); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:56:16: note: 'cellSize' is > 1 56 | assert(cellSize > 1); | ^ /usr/include/assert.h:100:27: note: expanded from macro 'assert' 100 | (static_cast (expr) \ | ^~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:56:9: note: '?' condition is true 56 | assert(cellSize > 1); | ^ /usr/include/assert.h:100:7: note: expanded from macro 'assert' 100 | (static_cast (expr) \ | ^ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:57:9: note: '?' condition is true 57 | assert(Misc::isPowerOfTwo(cellSize - 1)); | ^ /usr/include/assert.h:100:7: note: expanded from macro 'assert' 100 | (static_cast (expr) \ | ^ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:58:16: note: 'sampleSize' is not equal to 0 58 | assert(sampleSize != 0); | ^ /usr/include/assert.h:100:27: note: expanded from macro 'assert' 100 | (static_cast (expr) \ | ^~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:58:9: note: '?' condition is true 58 | assert(sampleSize != 0); | ^ /usr/include/assert.h:100:7: note: expanded from macro 'assert' 100 | (static_cast (expr) \ | ^ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:60:9: note: Calling 'sampleGrid<(lambda at /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:61:13)>' 60 | sampleGrid(sampleSize, beginX, beginY, endX, endY, | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 61 | [&](std::size_t globalX, std::size_t globalY, std::size_t vertX, std::size_t vertY) { | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 62 | const auto [cellX, x] = toCellAndLocal(beginX, globalX, cellSize); | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 63 | const auto [cellY, y] = toCellAndLocal(beginY, globalY, cellSize); | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 64 | f(cellX, cellY, x, y, vertX, vertY); | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 65 | }); | ~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:33:38: note: Assuming 'y' is < 'endY' 33 | for (std::size_t y = beginY; y < endY; y += sampleSize) | ^~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:33:9: note: Loop condition is true. Entering loop body 33 | for (std::size_t y = beginY; y < endY; y += sampleSize) | ^ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:36:42: note: Assuming 'x' is < 'endX' 36 | for (std::size_t x = beginX; x < endX; x += sampleSize) | ^~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:36:13: note: Loop condition is true. Entering loop body 36 | for (std::size_t x = beginX; x < endX; x += sampleSize) | ^ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:36:42: note: Assuming 'x' is < 'endX' 36 | for (std::size_t x = beginX; x < endX; x += sampleSize) | ^~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:36:13: note: Loop condition is true. Entering loop body 36 | for (std::size_t x = beginX; x < endX; x += sampleSize) | ^ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:37:17: note: Calling 'operator()' 37 | f(x, y, vertX++, vertY); | ^~~~~~~~~~~~~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:62:41: note: Calling 'toCellAndLocal' 62 | const auto [cellX, x] = toCellAndLocal(beginX, globalX, cellSize); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:18:35: note: The result of the '/' expression is undefined 18 | std::size_t cell = global / (cellSize - 1); | ~~~~~~~^~~~~~~~~~~~~~~~ --- components/esmterrain/gridsampling.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/esmterrain/gridsampling.hpp b/components/esmterrain/gridsampling.hpp index e71dfc5152..86544a214b 100644 --- a/components/esmterrain/gridsampling.hpp +++ b/components/esmterrain/gridsampling.hpp @@ -15,7 +15,9 @@ namespace ESMTerrain inline std::pair toCellAndLocal( std::size_t begin, std::size_t global, std::size_t cellSize) { + // NOLINTBEGIN(clang-analyzer-core.UndefinedBinaryOperatorResult) std::size_t cell = global / (cellSize - 1); + // NOLINTEND(clang-analyzer-core.UndefinedBinaryOperatorResult) std::size_t local = global & (cellSize - 2); if (global != begin && local == 0) { From 8682ea522f6c742e1ac1676e44ebc56207f72582 Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 27 Jul 2025 12:20:10 +0200 Subject: [PATCH 19/23] Remove unused namespace alias --- apps/mwiniimporter/importer.cpp | 2 -- apps/mwiniimporter/main.cpp | 1 - 2 files changed, 3 deletions(-) diff --git a/apps/mwiniimporter/importer.cpp b/apps/mwiniimporter/importer.cpp index a8dee709da..4b8e7acd61 100644 --- a/apps/mwiniimporter/importer.cpp +++ b/apps/mwiniimporter/importer.cpp @@ -9,8 +9,6 @@ #include #include -namespace sfs = std::filesystem; - namespace { // from configfileparser.cpp diff --git a/apps/mwiniimporter/main.cpp b/apps/mwiniimporter/main.cpp index 6e4242cb4e..c5f21ac67f 100644 --- a/apps/mwiniimporter/main.cpp +++ b/apps/mwiniimporter/main.cpp @@ -10,7 +10,6 @@ #include namespace bpo = boost::program_options; -namespace sfs = std::filesystem; #ifndef _WIN32 int main(int argc, char* argv[]) From c6f381f1c4d050f9a98690f126680bd1a8621c84 Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 27 Jul 2025 12:21:13 +0200 Subject: [PATCH 20/23] Ignore readability-identifier-naming for boost::program_options namespace alias --- .clang-tidy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-tidy b/.clang-tidy index 8597ea5c13..1f37003f8e 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -17,4 +17,4 @@ CheckOptions: - key: readability-identifier-naming.NamespaceCase value: CamelCase - key: readability-identifier-naming.NamespaceIgnoredRegexp - value: 'osg(DB|FX|Particle|Shadow|Viewer|Util)?' + value: 'bpo|osg(DB|FX|Particle|Shadow|Viewer|Util)?' From aae954643ce5cceafa023f4c7aa5961e66189136 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sun, 27 Jul 2025 12:47:35 +0200 Subject: [PATCH 21/23] Don't multiply magnitudes for effects that don't have magnitudes --- apps/openmw/mwmechanics/spelleffects.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index 99e5a09481..80dd67ef0d 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -377,8 +377,11 @@ namespace MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); return MWMechanics::MagicApplicationResult::Type::REMOVED; } - effect.mMinMagnitude *= magnitudeMult; - effect.mMaxMagnitude *= magnitudeMult; + else if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) + { + effect.mMinMagnitude *= magnitudeMult; + effect.mMaxMagnitude *= magnitudeMult; + } } return MWMechanics::MagicApplicationResult::Type::APPLIED; } From af58da58fd04d8c87a0ec493602df8ad624ef31f Mon Sep 17 00:00:00 2001 From: Claire Date: Sun, 27 Jul 2025 09:33:38 -0700 Subject: [PATCH 22/23] Fixed Unit Test in `apps/openmw_tests/mwworld/testptr.cpp` --- apps/openmw_tests/mwworld/testptr.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/openmw_tests/mwworld/testptr.cpp b/apps/openmw_tests/mwworld/testptr.cpp index 7bc0bebcec..9d1dd7aa60 100644 --- a/apps/openmw_tests/mwworld/testptr.cpp +++ b/apps/openmw_tests/mwworld/testptr.cpp @@ -7,6 +7,7 @@ #include #include +#include #include namespace MWWorld @@ -36,7 +37,7 @@ namespace MWWorld LiveCellRef liveCellRef(cellRef, &npc); liveCellRef.mData.setDeletedByContentFile(true); Ptr ptr(&liveCellRef); - EXPECT_EQ(ptr.toString(), "deleted object0xd00002a (NPC, \"player\")"); + EXPECT_THAT(ptr.toString(), StrCaseEq("deleted object0xd00002a (NPC, \"player\")")); } TEST(MWWorldPtrTest, toStringShouldReturnHumanReadableTextRepresentationOfPtr) @@ -53,7 +54,7 @@ namespace MWWorld cellRef.mRefNum = ESM::RefNum{ .mIndex = 0x2a, .mContentFile = 0xd }; LiveCellRef liveCellRef(cellRef, &npc); Ptr ptr(&liveCellRef); - EXPECT_EQ(ptr.toString(), "object0xd00002a (NPC, \"player\")"); + EXPECT_THAT(ptr.toString(), StrCaseEq("object0xd00002a (NPC, \"player\")")); } TEST(MWWorldPtrTest, underlyingLiveCellRefShouldBeDeregisteredOnDestruction) From e77ee5c20fdd9d7e7966e588d58a02339aaa3c6c Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 27 Jul 2025 18:40:39 +0200 Subject: [PATCH 23/23] Do not copy cell store to count refs --- apps/openmw/mwworld/esmstore.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index ea183b6b53..7262805f81 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -566,10 +566,10 @@ namespace MWWorld std::vector refs; std::set keyIDs; std::vector refIDs; - Store Cells = get(); - for (auto it = Cells.intBegin(); it != Cells.intEnd(); ++it) + const Store& cells = get(); + for (auto it = cells.intBegin(); it != cells.intEnd(); ++it) readRefs(*it, refs, refIDs, keyIDs, readers); - for (auto it = Cells.extBegin(); it != Cells.extEnd(); ++it) + for (auto it = cells.extBegin(); it != cells.extEnd(); ++it) readRefs(*it, refs, refIDs, keyIDs, readers); const auto lessByRefNum = [](const Ref& l, const Ref& r) { return l.mRefNum < r.mRefNum; }; std::stable_sort(refs.begin(), refs.end(), lessByRefNum);