Merge upstream/master

pull/541/head
AnyOldName3 6 years ago
commit 3785ba6aa0

@ -15,11 +15,11 @@ Debian:
- apt-get update -yq
- apt-get -o dir::cache::archives="$APT_CACHE_DIR" install -y cmake libboost-filesystem-dev libboost-program-options-dev libboost-system-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libsdl2-dev libqt4-dev libopenal-dev libopenscenegraph-3.4-dev libunshield-dev libtinyxml-dev
# - apt-get install -y libmygui-dev libbullet-dev # to be updated to latest below because stretch is too old
- curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet-dev_2.87+dfsg-2_amd64.deb -o libbullet-dev_2.87+dfsg-2_amd64.deb
- curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet2.87_2.87+dfsg-2_amd64.deb -o libbullet2.87_2.87+dfsg-2_amd64.deb
- curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb -o libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb
- curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb -o libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb
- curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui-dev_3.2.2+dfsg-1_amd64.deb -o libmygui-dev_3.2.2+dfsg-1_amd64.deb
- curl -L http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet-dev_2.87+dfsg-2_amd64.deb -o libbullet-dev_2.87+dfsg-2_amd64.deb
- curl -L http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet2.87_2.87+dfsg-2_amd64.deb -o libbullet2.87_2.87+dfsg-2_amd64.deb
- curl -L https://http.kali.org/pool/main/m/mygui/libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb -o libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb
- curl -L https://http.kali.org/pool/main/m/mygui/libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb -o libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb
- curl -L https://http.kali.org/pool/main/m/mygui/libmygui-dev_3.2.2+dfsg-1_amd64.deb -o libmygui-dev_3.2.2+dfsg-1_amd64.deb
- dpkg --ignore-depends=libmygui.ogreplatform0debian1v5 -i *.deb
stage: build
script:

@ -1,3 +1,10 @@
0.46.0
------
Feature #2229: Improve pathfinding AI
Feature #3442: Default values for fallbacks from ini file
0.45.0
------
@ -8,6 +15,7 @@
Bug #2256: Landing sound not playing when jumping immediately after landing
Bug #2274: Thin platform clips through player character instead of lifting
Bug #2326: After a bound item expires the last equipped item of that type is not automatically re-equipped
Bug #2446: Restore Attribute/Skill should allow restoring drained attributes
Bug #2455: Creatures attacks degrade armor
Bug #2562: Forcing AI to activate a teleport door sometimes causes a crash
Bug #2626: Resurrecting the player does not resume the game
@ -33,6 +41,7 @@
Bug #3788: GetPCInJail and GetPCTraveling do not work as in vanilla
Bug #3836: Script fails to compile when command argument contains "\n"
Bug #3876: Landscape texture painting is misaligned
Bug #3890: Magic light source attenuation is inaccurate
Bug #3897: Have Goodbye give all choices the effects of Goodbye
Bug #3911: [macOS] Typing in the "Content List name" dialog box produces double characters
Bug #3920: RemoveSpellEffects doesn't remove constant effects
@ -45,6 +54,7 @@
Bug #4110: Fixed undo / redo menu text losing the assigned shortcuts
Bug #4125: OpenMW logo cropped on bugtracker
Bug #4215: OpenMW shows book text after last EOL tag
Bug #4217: Fixme implementation differs from Morrowind's
Bug #4221: Characters get stuck in V-shaped terrain
Bug #4230: AiTravel package issues break some Tribunal quests
Bug #4231: Infected rats from the "Crimson Plague" quest rendered unconscious by change in Drain Fatigue functionality
@ -54,6 +64,7 @@
Bug #4274: Pre-0.43 death animations are not forward-compatible with 0.43+
Bug #4286: Scripted animations can be interrupted
Bug #4291: Non-persistent actors that started the game as dead do not play death animations
Bug #4292: CenterOnCell implementation differs from vanilla
Bug #4293: Faction members are not aware of faction ownerships in barter
Bug #4304: "Follow" not working as a second AI package
Bug #4307: World cleanup should remove dead bodies only if death animation is finished
@ -64,7 +75,7 @@
Bug #4368: Settings window ok button doesn't have key focus by default
Bug #4378: On-self absorb spells restore stats
Bug #4393: NPCs walk back to where they were after using ResetActors
Bug #4416: Handle exception if we try to play non-music file
Bug #4416: Non-music files crash the game when they are tried to be played
Bug #4419: MRK NiStringExtraData is handled incorrectly
Bug #4426: RotateWorld behavior is incorrect
Bug #4429: [Windows] Error on build INSTALL.vcxproj project (debug) with cmake 3.7.2
@ -80,6 +91,7 @@
Bug #4459: NotCell dialogue condition doesn't support partial matches
Bug #4460: Script function "Equip" doesn't bypass beast restrictions
Bug #4461: "Open" spell from non-player caster isn't a crime
Bug #4463: %g format doesn't return more digits
Bug #4464: OpenMW keeps AiState cached storages even after we cancel AI packages
Bug #4467: Content selector: cyrillic characters are decoded incorrectly in plugin descriptions
Bug #4469: Abot Silt Striders Model turn 90 degrees on horizontal
@ -143,12 +155,17 @@
Bug #4674: Journal can be opened when settings window is open
Bug #4677: Crash in ESM reader when NPC record has DNAM record without DODT one
Bug #4678: Crash in ESP parser when SCVR has no variable names
Bug #4684: Spell Absorption is additive
Bug #4685: Missing sound causes an exception inside Say command
Bug #4689: Default creature soundgen entries are not used
Bug #4691: Loading bar for cell should be moved up when text is still active at bottom of screen
Feature #912: Editor: Add missing icons to UniversalId tables
Feature #1221: Editor: Creature/NPC rendering
Feature #1617: Editor: Enchantment effect record verifier
Feature #1645: Casting effects from objects
Feature #2606: Editor: Implemented (optional) case sensitive global search
Feature #2787: Use the autogenerated collision box, if the creature mesh has no predefined one
Feature #2845: Editor: add record view and preview default keybindings
Feature #2847: Content selector: allow to copy the path to a file by using the context menu
Feature #3083: Play animation when NPC is casting spell via script
Feature #3103: Provide option for disposition to get increased by successful trade
@ -178,7 +195,9 @@
Feature #4632: AI priority: utilize vanilla AI GMSTs for priority rating
Feature #4636: Use sTo GMST in spellmaking menu
Feature #4642: Batching potion creation
Feature #4647: Cull actors outside of AI processing range
Feature #4682: Use the collision box from basic creature mesh if the X one have no collisions
Feature #4697: Use the real thrown weapon damage in tooltips and AI
Task #2490: Don't open command prompt window on Release-mode builds automatically
Task #4545: Enable is_pod string test
Task #4605: Optimize skinning

@ -1,3 +1,3 @@
#!/bin/sh
#!/bin/sh -e
sudo ln -s /usr/bin/clang-3.6 /usr/local/bin/clang
sudo ln -s /usr/bin/clang++-3.6 /usr/local/bin/clang++

@ -1,4 +1,4 @@
#!/bin/sh
#!/bin/sh -e
brew update
@ -6,5 +6,5 @@ brew outdated cmake || brew upgrade cmake
brew outdated pkgconfig || brew upgrade pkgconfig
brew install qt
curl -fSL -R -J https://downloads.openmw.org/osx/dependencies/openmw-deps-100d2e0.zip -o ~/openmw-deps.zip
curl -fSL -R -J https://downloads.openmw.org/osx/dependencies/openmw-deps-4eec887.zip -o ~/openmw-deps.zip
unzip -o ~/openmw-deps.zip -d /private/tmp/openmw-deps > /dev/null

@ -5,6 +5,9 @@ free -m
env GENERATOR='Unix Makefiles' CONFIGURATION=Release CI/build_googletest.sh
GOOGLETEST_DIR="$(pwd)/googletest/build"
env GENERATOR='Unix Makefiles' CONFIGURATION=Release CI/build_recastnavigation.sh
RECASTNAVIGATION_DIR="$(pwd)/recastnavigation/build"
mkdir build
cd build
export CODE_COVERAGE=1
@ -18,4 +21,5 @@ ${ANALYZE}cmake \
-DUSE_SYSTEM_TINYXML=TRUE \
-DGTEST_ROOT="${GOOGLETEST_DIR}" \
-DGMOCK_ROOT="${GOOGLETEST_DIR}" \
-DRecastNavigation_ROOT="${RECASTNAVIGATION_DIR}" \
..

@ -304,6 +304,10 @@ if ! [ -z $UNITY_BUILD ]; then
add_cmake_opts "-DOPENMW_UNITY_BUILD=True"
fi
if [ ${BITS} -eq 64 ]; then
GENERATOR="${GENERATOR} Win64"
fi
echo
echo "==================================="
echo "Starting prebuild on MSVC${MSVC_DISPLAY_YEAR} WIN${BITS}"
@ -644,6 +648,13 @@ printf "SDL 2.0.7... "
echo Done.
}
echo
# recastnavigation
printf 'recastnavigation...'
{
env GENERATOR="${GENERATOR}" CONFIGURATION="${CONFIGURATION}" ${DEPS_INSTALL}/../../CI/build_recastnavigation.sh
add_cmake_opts -DRecastNavigation_ROOT="$(pwd)/recastnavigation/build"
}
echo
cd $DEPS_INSTALL/..
echo
echo "Setting up OpenMW build..."

@ -1,8 +1,11 @@
#!/bin/sh
#!/bin/sh -e
export CXX=clang++
export CC=clang
env GENERATOR='Unix Makefiles' CONFIGURATION=Release CI/build_recastnavigation.sh
RECASTNAVIGATION_DIR="$(pwd)/recastnavigation/build"
DEPENDENCIES_ROOT="/private/tmp/openmw-deps/openmw-deps"
QT_PATH=`brew --prefix qt`
mkdir build
@ -17,5 +20,6 @@ cmake \
-D DESIRED_QT_VERSION=5 \
-D BUILD_ESMTOOL=FALSE \
-D BUILD_MYGUI_PLUGIN=FALSE \
-D RecastNavigation_ROOT="${RECASTNAVIGATION_DIR}" \
-G"Unix Makefiles" \
..

@ -0,0 +1,17 @@
#!/bin/sh -e
git clone https://github.com/recastnavigation/recastnavigation.git
cd recastnavigation
mkdir build
cd build
cmake \
-DCMAKE_BUILD_TYPE="${CONFIGURATION}" \
-DRECASTNAVIGATION_DEMO=OFF \
-DRECASTNAVIGATION_TESTS=OFF \
-DRECASTNAVIGATION_EXAMPLES=OFF \
-DRECASTNAVIGATION_STATIC=ON \
-DCMAKE_INSTALL_PREFIX=. \
-G "${GENERATOR}" \
..
cmake --build . --config "${CONFIGURATION}"
cmake --build . --target install --config "${CONFIGURATION}"

@ -7,6 +7,7 @@
#include <components/debug/debuglog.hpp>
#include <components/fallback/validate.hpp>
#include <components/misc/rng.hpp>
#include <components/nifosg/nifloader.hpp>
#include "model/doc/document.hpp"
@ -355,6 +356,8 @@ int CS::Editor::run()
if (mLocal.empty())
return 1;
Misc::Rng::init();
mStartup.show();
QApplication::setQuitOnLastWindowClosed (true);

@ -317,8 +317,8 @@ void CSMPrefs::State::declare()
declareShortcut ("table-remove", "Remove Row/Record", QKeySequence(Qt::Key_Delete));
declareShortcut ("table-moveup", "Move Record Up", QKeySequence());
declareShortcut ("table-movedown", "Move Record Down", QKeySequence());
declareShortcut ("table-view", "View Record", QKeySequence());
declareShortcut ("table-preview", "Preview Record", QKeySequence());
declareShortcut ("table-view", "View Record", QKeySequence(Qt::ShiftModifier | Qt::Key_C));
declareShortcut ("table-preview", "Preview Record", QKeySequence(Qt::ShiftModifier | Qt::Key_V));
declareShortcut ("table-extendeddelete", "Extended Record Deletion", QKeySequence());
declareShortcut ("table-extendedrevert", "Extended Record Revertion", QKeySequence());

@ -292,6 +292,7 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id,
mEditAction = new QAction (tr ("Edit Record"), this);
connect (mEditAction, SIGNAL (triggered()), this, SLOT (editRecord()));
mEditAction->setIcon(QIcon(":edit-edit"));
addAction (mEditAction);
CSMPrefs::Shortcut* editShortcut = new CSMPrefs::Shortcut("table-edit", this);
editShortcut->associateAction(mEditAction);
@ -300,12 +301,14 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id,
{
mCreateAction = new QAction (tr ("Add Record"), this);
connect (mCreateAction, SIGNAL (triggered()), this, SIGNAL (createRequest()));
mCreateAction->setIcon(QIcon(":edit-add"));
addAction (mCreateAction);
CSMPrefs::Shortcut* createShortcut = new CSMPrefs::Shortcut("table-add", this);
createShortcut->associateAction(mCreateAction);
mCloneAction = new QAction (tr ("Clone Record"), this);
connect(mCloneAction, SIGNAL (triggered()), this, SLOT (cloneRecord()));
mCloneAction->setIcon(QIcon(":edit-clone"));
addAction(mCloneAction);
CSMPrefs::Shortcut* cloneShortcut = new CSMPrefs::Shortcut("table-clone", this);
cloneShortcut->associateAction(mCloneAction);
@ -315,6 +318,7 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id,
{
mTouchAction = new QAction(tr("Touch Record"), this);
connect(mTouchAction, SIGNAL(triggered()), this, SLOT(touchRecord()));
mTouchAction->setIcon(QIcon(":edit-touch"));
addAction(mTouchAction);
CSMPrefs::Shortcut* touchShortcut = new CSMPrefs::Shortcut("table-touch", this);
touchShortcut->associateAction(mTouchAction);
@ -322,49 +326,56 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id,
mRevertAction = new QAction (tr ("Revert Record"), this);
connect (mRevertAction, SIGNAL (triggered()), mDispatcher, SLOT (executeRevert()));
mRevertAction->setIcon(QIcon(":edit-undo"));
addAction (mRevertAction);
CSMPrefs::Shortcut* revertShortcut = new CSMPrefs::Shortcut("table-revert", this);
revertShortcut->associateAction(mRevertAction);
mDeleteAction = new QAction (tr ("Delete Record"), this);
connect (mDeleteAction, SIGNAL (triggered()), mDispatcher, SLOT (executeDelete()));
mDeleteAction->setIcon(QIcon(":edit-delete"));
addAction (mDeleteAction);
CSMPrefs::Shortcut* deleteShortcut = new CSMPrefs::Shortcut("table-remove", this);
deleteShortcut->associateAction(mDeleteAction);
mMoveUpAction = new QAction (tr ("Move Up"), this);
connect (mMoveUpAction, SIGNAL (triggered()), this, SLOT (moveUpRecord()));
mMoveUpAction->setIcon(QIcon(":record-up"));
addAction (mMoveUpAction);
CSMPrefs::Shortcut* moveUpShortcut = new CSMPrefs::Shortcut("table-moveup", this);
moveUpShortcut->associateAction(mMoveUpAction);
mMoveDownAction = new QAction (tr ("Move Down"), this);
connect (mMoveDownAction, SIGNAL (triggered()), this, SLOT (moveDownRecord()));
mMoveDownAction->setIcon(QIcon(":record-down"));
addAction (mMoveDownAction);
CSMPrefs::Shortcut* moveDownShortcut = new CSMPrefs::Shortcut("table-movedown", this);
moveDownShortcut->associateAction(mMoveDownAction);
mViewAction = new QAction (tr ("View"), this);
connect (mViewAction, SIGNAL (triggered()), this, SLOT (viewRecord()));
mViewAction->setIcon(QIcon(":/cell.png"));
addAction (mViewAction);
CSMPrefs::Shortcut* viewShortcut = new CSMPrefs::Shortcut("table-view", this);
viewShortcut->associateAction(mViewAction);
mPreviewAction = new QAction (tr ("Preview"), this);
connect (mPreviewAction, SIGNAL (triggered()), this, SLOT (previewRecord()));
mPreviewAction->setIcon(QIcon(":edit-preview"));
addAction (mPreviewAction);
CSMPrefs::Shortcut* previewShortcut = new CSMPrefs::Shortcut("table-preview", this);
previewShortcut->associateAction(mPreviewAction);
mExtendedDeleteAction = new QAction (tr ("Extended Delete Record"), this);
connect (mExtendedDeleteAction, SIGNAL (triggered()), this, SLOT (executeExtendedDelete()));
mExtendedDeleteAction->setIcon(QIcon(":edit-delete"));
addAction (mExtendedDeleteAction);
CSMPrefs::Shortcut* extendedDeleteShortcut = new CSMPrefs::Shortcut("table-extendeddelete", this);
extendedDeleteShortcut->associateAction(mExtendedDeleteAction);
mExtendedRevertAction = new QAction (tr ("Extended Revert Record"), this);
connect (mExtendedRevertAction, SIGNAL (triggered()), this, SLOT (executeExtendedRevert()));
mExtendedRevertAction->setIcon(QIcon(":edit-undo"));
addAction (mExtendedRevertAction);
CSMPrefs::Shortcut* extendedRevertShortcut = new CSMPrefs::Shortcut("table-extendedrevert", this);
extendedRevertShortcut->associateAction(mExtendedRevertAction);

@ -21,7 +21,7 @@ add_openmw_dir (mwrender
actors objects renderingmanager animation rotatecontroller sky npcanimation vismask
creatureanimation effectmanager util renderinginterface pathgrid rendermode weaponanimation
bulletdebugdraw globalmap characterpreview camera localmap water terrainstorage ripplesimulation
renderbin actoranimation landmanager
renderbin actoranimation landmanager navmesh actorspaths
)
add_openmw_dir (mwinput
@ -70,7 +70,7 @@ add_openmw_dir (mwworld
)
add_openmw_dir (mwphysics
physicssystem trace collisiontype actor convert
physicssystem trace collisiontype actor convert object heightfield
)
add_openmw_dir (mwclass
@ -117,6 +117,10 @@ include_directories(
${FFmpeg_INCLUDE_DIRS}
)
find_package(RecastNavigation COMPONENTS DebugUtils Detour Recast REQUIRED)
include_directories(SYSTEM ${RecastNavigation_INCLUDE_DIRS})
target_link_libraries(openmw
${OSG_LIBRARIES}
${OPENTHREADS_LIBRARIES}
@ -134,6 +138,7 @@ target_link_libraries(openmw
${FFmpeg_LIBRARIES}
${MyGUI_LIBRARIES}
${SDL2_LIBRARY}
${RecastNavigation_LIBRARIES}
"osg-ffmpeg-videoplayer"
"oics"
components

@ -233,6 +233,10 @@ namespace MWBase
virtual void castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell) = 0;
virtual void processChangedSettings (const std::set< std::pair<std::string, std::string> >& settings) = 0;
virtual float getActorsProcessingRange() const = 0;
/// Check if the target actor was detected by an observer
/// If the observer is a non-NPC, check all actors in AI processing distance as observers
virtual bool isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) = 0;

@ -285,6 +285,8 @@ namespace MWBase
virtual void setEnemy (const MWWorld::Ptr& enemy) = 0;
virtual int getMessagesCount() const = 0;
virtual const Translation::Storage& getTranslationDataStorage() const = 0;
/// Warning: do not use MyGUI::InputManager::setKeyFocusWidget directly. Instead use this.

@ -4,6 +4,7 @@
#include <vector>
#include <map>
#include <set>
#include <deque>
#include <components/esm/cellid.hpp>
@ -54,6 +55,11 @@ namespace MWMechanics
struct Movement;
}
namespace DetourNavigator
{
class Navigator;
}
namespace MWWorld
{
class CellStore;
@ -302,6 +308,9 @@ namespace MWBase
virtual bool castRay (float x1, float y1, float z1, float x2, float y2, float z2) = 0;
virtual void setActorCollisionMode(const MWWorld::Ptr& ptr, bool enabled) = 0;
virtual bool isActorCollisionEnabled(const MWWorld::Ptr& ptr) = 0;
virtual bool toggleCollisionMode() = 0;
///< Toggle collision mode for player. If disabled player object should ignore
/// collisions and gravity.
@ -592,6 +601,13 @@ namespace MWBase
/// Preload VFX associated with this effect list
virtual void preloadEffects(const ESM::EffectList* effectList) = 0;
virtual DetourNavigator::Navigator* getNavigator() const = 0;
virtual void updateActorPath(const MWWorld::ConstPtr& actor, const std::deque<osg::Vec3f>& path,
const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const = 0;
virtual void setNavMeshNumberToRender(const std::size_t value) = 0;
};
}

@ -138,7 +138,7 @@ namespace MWClass
std::string Activator::getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const
{
std::string model = getModel(ptr); // Assume it's not empty, since we wouldn't have gotten the soundgen otherwise
const std::string model = getModel(ptr); // Assume it's not empty, since we wouldn't have gotten the soundgen otherwise
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
std::string creatureId;
@ -151,21 +151,35 @@ namespace MWClass
}
}
if (creatureId.empty())
return std::string();
int type = getSndGenTypeFromName(name);
std::vector<const ESM::SoundGenerator*> sounds;
std::vector<const ESM::SoundGenerator*> fallbacksounds;
if (!creatureId.empty())
{
std::vector<const ESM::SoundGenerator*> sounds;
for (auto sound = store.get<ESM::SoundGenerator>().begin(); sound != store.get<ESM::SoundGenerator>().end(); ++sound)
{
if (type == sound->mType && !sound->mCreature.empty() && (Misc::StringUtils::ciEqual(creatureId, sound->mCreature)))
sounds.push_back(&*sound);
if (type == sound->mType && sound->mCreature.empty())
fallbacksounds.push_back(&*sound);
}
if (!sounds.empty())
return sounds[Misc::Rng::rollDice(sounds.size())]->mSound;
if (!fallbacksounds.empty())
return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size())]->mSound;
}
else
{
// The activator doesn't have a corresponding creature ID, but we can try to use the defaults
for (auto sound = store.get<ESM::SoundGenerator>().begin(); sound != store.get<ESM::SoundGenerator>().end(); ++sound)
if (type == sound->mType && sound->mCreature.empty())
fallbacksounds.push_back(&*sound);
if (type == ESM::SoundGenerator::Land)
return "Body Fall Large";
if (!fallbacksounds.empty())
return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size())]->mSound;
}
return std::string();
}

@ -297,11 +297,11 @@ namespace MWClass
{
const MWWorld::InventoryStore& invStore = npc.getClass().getInventoryStore(npc);
if (ptr.getCellRef().getCharge() == 0)
if (getItemHealth(ptr) == 0)
return std::make_pair(0, "#{sInventoryMessage1}");
// slots that this item can be equipped in
std::pair<std::vector<int>, bool> slots_ = ptr.getClass().getEquipmentSlots(ptr);
std::pair<std::vector<int>, bool> slots_ = getEquipmentSlots(ptr);
if (slots_.first.empty())
return std::make_pair(0, "");

@ -215,7 +215,7 @@ namespace MWClass
std::pair<int, std::string> Clothing::canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const
{
// slots that this item can be equipped in
std::pair<std::vector<int>, bool> slots_ = ptr.getClass().getEquipmentSlots(ptr);
std::pair<std::vector<int>, bool> slots_ = getEquipmentSlots(ptr);
if (slots_.first.empty())
return std::make_pair(0, "");

@ -137,7 +137,7 @@ namespace MWClass
// Persistent actors with 0 health do not play death animation
if (data->mCreatureStats.isDead())
data->mCreatureStats.setDeathAnimationFinished(ptr.getClass().isPersistent(ptr));
data->mCreatureStats.setDeathAnimationFinished(isPersistent(ptr));
// spells
for (std::vector<std::string>::const_iterator iter (ref->mBase->mSpells.mList.begin());
@ -194,9 +194,9 @@ namespace MWClass
models.push_back(model);
// FIXME: use const version of InventoryStore functions once they are available
if (ptr.getClass().hasInventoryStore(ptr))
if (hasInventoryStore(ptr))
{
const MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore(ptr);
const MWWorld::InventoryStore& invStore = getInventoryStore(ptr);
for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot)
{
MWWorld::ConstContainerStoreIterator equipped = invStore.getSlot(slot);
@ -237,7 +237,7 @@ namespace MWClass
// Get the weapon used (if hand-to-hand, weapon = inv.end())
MWWorld::Ptr weapon;
if (ptr.getClass().hasInventoryStore(ptr))
if (hasInventoryStore(ptr))
{
MWWorld::InventoryStore &inv = getInventoryStore(ptr);
MWWorld::ContainerStoreIterator weaponslot = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight);
@ -253,8 +253,7 @@ namespace MWClass
// For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result.
std::vector<MWWorld::Ptr> targetActors;
if (!ptr.isEmpty() && ptr.getClass().isActor())
ptr.getClass().getCreatureStats(ptr).getAiSequence().getCombatTargets(targetActors);
stats.getAiSequence().getCombatTargets(targetActors);
std::pair<MWWorld::Ptr, osg::Vec3f> result = MWBase::Environment::get().getWorld()->getHitContact(ptr, dist, targetActors);
if (result.first.isEmpty())
@ -332,7 +331,7 @@ namespace MWClass
void Creature::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const
{
MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr);
MWMechanics::CreatureStats& stats = getCreatureStats(ptr);
// NOTE: 'object' and/or 'attacker' may be empty.
if (!attacker.isEmpty() && attacker.getClass().isActor() && !stats.getAiSequence().isInCombat(attacker))
@ -346,7 +345,7 @@ namespace MWClass
setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker);
// Attacker and target store each other as hitattemptactor if they have no one stored yet
if (!attacker.isEmpty() && attacker.getClass().isActor() && !ptr.isEmpty() && ptr.getClass().isActor())
if (!attacker.isEmpty() && attacker.getClass().isActor())
{
MWMechanics::CreatureStats& statsAttacker = attacker.getClass().getCreatureStats(attacker);
// First handle the attacked actor
@ -522,7 +521,7 @@ namespace MWClass
const MWBase::World *world = MWBase::Environment::get().getWorld();
const MWMechanics::MagicEffects &mageffects = stats.getMagicEffects();
bool running = ptr.getClass().getCreatureStats(ptr).getStance(MWMechanics::CreatureStats::Stance_Run);
bool running = stats.getStance(MWMechanics::CreatureStats::Stance_Run);
// The Run speed difference for creatures comes from the animation speed difference (see runStateToWalkState in character.cpp)
float runSpeed = walkSpeed;
@ -632,25 +631,27 @@ namespace MWClass
if(type >= 0)
{
std::vector<const ESM::SoundGenerator*> sounds;
std::vector<const ESM::SoundGenerator*> fallbacksounds;
MWWorld::LiveCellRef<ESM::Creature>* ref = ptr.get<ESM::Creature>();
const std::string& ourId = (ref->mBase->mOriginal.empty()) ? ptr.getCellRef().getRefId() : ref->mBase->mOriginal;
MWWorld::Store<ESM::SoundGenerator>::iterator sound = store.begin();
while(sound != store.end())
while (sound != store.end())
{
if (type == sound->mType && !sound->mCreature.empty() && (Misc::StringUtils::ciEqual(ourId, sound->mCreature)))
sounds.push_back(&*sound);
if (type == sound->mType && sound->mCreature.empty())
fallbacksounds.push_back(&*sound);
++sound;
}
if(!sounds.empty())
if (!sounds.empty())
return sounds[Misc::Rng::rollDice(sounds.size())]->mSound;
if (!fallbacksounds.empty())
return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size())]->mSound;
}
if (type == ESM::SoundGenerator::Land)
return "Body Fall Large";
return "";
}
@ -806,7 +807,7 @@ namespace MWClass
void Creature::respawn(const MWWorld::Ptr &ptr) const
{
const MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr);
const MWMechanics::CreatureStats& creatureStats = getCreatureStats(ptr);
if (ptr.getRefData().getCount() > 0 && !creatureStats.isDead())
return;

@ -357,7 +357,7 @@ namespace MWClass
// Persistent actors with 0 health do not play death animation
if (data->mNpcStats.isDead())
data->mNpcStats.setDeathAnimationFinished(ptr.getClass().isPersistent(ptr));
data->mNpcStats.setDeathAnimationFinished(isPersistent(ptr));
// race powers
const ESM::Race *race = MWBase::Environment::get().getWorld()->getStore().get<ESM::Race>().find(ref->mBase->mRace);
@ -469,9 +469,7 @@ namespace MWClass
// FIXME: use const version of InventoryStore functions once they are available
// preload equipped items
if (ptr.getClass().hasInventoryStore(ptr))
{
const MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore(ptr);
const MWWorld::InventoryStore& invStore = getInventoryStore(ptr);
for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot)
{
MWWorld::ConstContainerStoreIterator equipped = invStore.getSlot(slot);
@ -506,7 +504,6 @@ namespace MWClass
}
}
}
}
// preload body parts
if (race)
@ -573,8 +570,8 @@ namespace MWClass
// For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result.
std::vector<MWWorld::Ptr> targetActors;
if (!ptr.isEmpty() && ptr.getClass().isActor() && ptr != MWMechanics::getPlayer())
ptr.getClass().getCreatureStats(ptr).getAiSequence().getCombatTargets(targetActors);
if (ptr != MWMechanics::getPlayer())
getCreatureStats(ptr).getAiSequence().getCombatTargets(targetActors);
// TODO: Use second to work out the hit angle
std::pair<MWWorld::Ptr, osg::Vec3f> result = world->getHitContact(ptr, dist, targetActors);
@ -597,7 +594,7 @@ namespace MWClass
if(!weapon.isEmpty())
weapskill = weapon.getClass().getEquipmentSkill(weapon);
float hitchance = MWMechanics::getHitChance(ptr, victim, ptr.getClass().getSkill(ptr, weapskill));
float hitchance = MWMechanics::getHitChance(ptr, victim, getSkill(ptr, weapskill));
if (Misc::Rng::roll0to99() >= hitchance)
{
@ -667,7 +664,7 @@ namespace MWClass
void Npc::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const
{
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr);
MWMechanics::CreatureStats& stats = getCreatureStats(ptr);
bool wasDead = stats.isDead();
// Note OnPcHitMe is not set for friendly hits.
@ -681,7 +678,7 @@ namespace MWClass
}
// Attacker and target store each other as hitattemptactor if they have no one stored yet
if (!attacker.isEmpty() && attacker.getClass().isActor() && !ptr.isEmpty() && ptr.getClass().isActor())
if (!attacker.isEmpty() && attacker.getClass().isActor())
{
MWMechanics::CreatureStats& statsAttacker = attacker.getClass().getCreatureStats(attacker);
// First handle the attacked actor
@ -702,7 +699,7 @@ namespace MWClass
if (setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer())
{
const std::string &script = ptr.getClass().getScript(ptr);
const std::string &script = getScript(ptr);
/* Set the OnPCHitMe script variable. The script is responsible for clearing it. */
if(!script.empty())
ptr.getRefData().getLocals().setVarByInt(script, "onpchitme", 1);
@ -891,12 +888,11 @@ namespace MWClass
if(stats.getAiSequence().isInCombat())
return std::shared_ptr<MWWorld::Action>(new MWWorld::FailedAction(""));
if(getCreatureStats(actor).getStance(MWMechanics::CreatureStats::Stance_Sneak)
|| ptr.getClass().getCreatureStats(ptr).getKnockedDown())
if(getCreatureStats(actor).getStance(MWMechanics::CreatureStats::Stance_Sneak) || stats.getKnockedDown())
return std::shared_ptr<MWWorld::Action>(new MWWorld::ActionOpen(ptr)); // stealing
// Can't talk to werewolfs
if(ptr.getClass().isNpc() && ptr.getClass().getNpcStats(ptr).isWerewolf())
if(getNpcStats(ptr).isWerewolf())
return std::shared_ptr<MWWorld::Action> (new MWWorld::FailedAction(""));
return std::shared_ptr<MWWorld::Action>(new MWWorld::ActionTalk(ptr));
@ -927,7 +923,7 @@ namespace MWClass
float Npc::getSpeed(const MWWorld::Ptr& ptr) const
{
const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr);
const MWMechanics::CreatureStats& stats = getCreatureStats(ptr);
if (stats.isParalyzed() || stats.getKnockedDown() || stats.isDead())
return 0.f;
@ -993,7 +989,7 @@ namespace MWClass
if(getEncumbrance(ptr) > getCapacity(ptr))
return 0.f;
const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr);
const MWMechanics::CreatureStats& stats = getCreatureStats(ptr);
if (stats.isParalyzed() || stats.getKnockedDown() || stats.isDead())
return 0.f;
@ -1148,9 +1144,7 @@ namespace MWClass
const bool hasHealth = it->getClass().hasItemHealth(*it);
if (hasHealth)
{
int armorHealth = it->getClass().getItemHealth(*it);
int armorMaxHealth = it->getClass().getItemMaxHealth(*it);
ratings[i] *= (float(armorHealth) / armorMaxHealth);
ratings[i] *= it->getClass().getItemNormalizedHealth(*it);
}
}
}
@ -1215,13 +1209,13 @@ namespace MWClass
return (name == "left") ? "FootWaterLeft" : "FootWaterRight";
if(world->isOnGround(ptr))
{
if (ptr.getClass().getNpcStats(ptr).isWerewolf()
&& ptr.getClass().getCreatureStats(ptr).getStance(MWMechanics::CreatureStats::Stance_Run))
if (getNpcStats(ptr).isWerewolf()
&& getCreatureStats(ptr).getStance(MWMechanics::CreatureStats::Stance_Run))
{
MWMechanics::WeaponType weaponType = MWMechanics::WeapType_None;
MWMechanics::getActiveWeapon(ptr.getClass().getCreatureStats(ptr), ptr.getClass().getInventoryStore(ptr), &weaponType);
MWMechanics::getActiveWeapon(getCreatureStats(ptr), getInventoryStore(ptr), &weaponType);
if (weaponType == MWMechanics::WeapType_None)
return "";
return std::string();
}
const MWWorld::InventoryStore &inv = Npc::getInventoryStore(ptr);
@ -1239,12 +1233,12 @@ namespace MWClass
return (name == "left") ? "FootHeavyLeft" : "FootHeavyRight";
}
}
return "";
return std::string();
}
// Morrowind ignores land soundgen for NPCs
if(name == "land")
return "";
return std::string();
if(name == "swimleft")
return "Swim Left";
if(name == "swimright")
@ -1254,11 +1248,11 @@ namespace MWClass
// only for biped creatures?
if(name == "moan")
return "";
return std::string();
if(name == "roar")
return "";
return std::string();
if(name == "scream")
return "";
return std::string();
throw std::runtime_error(std::string("Unexpected soundgen type: ")+name);
}
@ -1272,7 +1266,7 @@ namespace MWClass
int Npc::getSkill(const MWWorld::Ptr& ptr, int skill) const
{
return ptr.getClass().getNpcStats(ptr).getSkill(skill).getModified();
return getNpcStats(ptr).getSkill(skill).getModified();
}
int Npc::getBloodTexture(const MWWorld::ConstPtr &ptr) const
@ -1354,7 +1348,7 @@ namespace MWClass
void Npc::respawn(const MWWorld::Ptr &ptr) const
{
const MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr);
const MWMechanics::CreatureStats& creatureStats = getCreatureStats(ptr);
if (ptr.getRefData().getCount() > 0 && !creatureStats.isDead())
return;

@ -265,7 +265,7 @@ namespace MWClass
std::string text;
// weapon type & damage
if ((ref->mBase->mData.mType < 12 || Settings::Manager::getBool("show projectile damage", "Game")) && ref->mBase->mData.mType < 14)
if ((ref->mBase->mData.mType < ESM::Weapon::Arrow || Settings::Manager::getBool("show projectile damage", "Game")) && ref->mBase->mData.mType <= ESM::Weapon::Bolt)
{
text += "\n#{sType} ";
@ -295,7 +295,15 @@ namespace MWClass
((oneOrTwoHanded != "") ? ", " + store.get<ESM::GameSetting>().find(oneOrTwoHanded)->mValue.getString() : "");
// weapon damage
if (ref->mBase->mData.mType >= 9)
if (ref->mBase->mData.mType == ESM::Weapon::MarksmanThrown)
{
// Thrown weapons have 2x real damage applied
// as they're both the weapon and the ammo
text += "\n#{sAttack}: "
+ MWGui::ToolTips::toString(static_cast<int>(ref->mBase->mData.mChop[0] * 2))
+ " - " + MWGui::ToolTips::toString(static_cast<int>(ref->mBase->mData.mChop[1] * 2));
}
else if (ref->mBase->mData.mType >= ESM::Weapon::MarksmanBow)
{
// marksman
text += "\n#{sAttack}: "
@ -383,7 +391,7 @@ namespace MWClass
std::pair<int, std::string> Weapon::canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const
{
if (hasItemHealth(ptr) && ptr.getCellRef().getCharge() == 0)
if (hasItemHealth(ptr) && getItemHealth(ptr) == 0)
return std::make_pair(0, "#{sInventoryMessage1}");
// Do not allow equip weapons from inventory during attack
@ -391,7 +399,7 @@ namespace MWClass
&& MWBase::Environment::get().getWindowManager()->isGuiMode())
return std::make_pair(0, "#{sCantEquipWeapWarning}");
std::pair<std::vector<int>, bool> slots_ = ptr.getClass().getEquipmentSlots(ptr);
std::pair<std::vector<int>, bool> slots_ = getEquipmentSlots(ptr);
if (slots_.first.empty())
return std::make_pair (0, "");

@ -96,7 +96,7 @@ namespace MWGui
Log(Debug::Warning) << "Warning: no splash screens found!";
}
void LoadingScreen::setLabel(const std::string &label, bool important)
void LoadingScreen::setLabel(const std::string &label, bool important, bool center)
{
mImportantLabel = important;
@ -105,7 +105,11 @@ namespace MWGui
MyGUI::IntSize size(mLoadingText->getTextSize().width+padding, mLoadingBox->getHeight());
size.width = std::max(300, size.width);
mLoadingBox->setSize(size);
mLoadingBox->setPosition(mMainWidget->getWidth()/2 - mLoadingBox->getWidth()/2, mLoadingBox->getTop());
if (center)
mLoadingBox->setPosition(mMainWidget->getWidth()/2 - mLoadingBox->getWidth()/2, mMainWidget->getHeight()/2 - mLoadingBox->getHeight()/2);
else
mLoadingBox->setPosition(mMainWidget->getWidth()/2 - mLoadingBox->getWidth()/2, mMainWidget->getHeight() - mLoadingBox->getHeight() - 8);
}
void LoadingScreen::setVisible(bool visible)

@ -34,7 +34,7 @@ namespace MWGui
virtual ~LoadingScreen();
/// Overridden from Loading::Listener, see the Loading::Listener documentation for usage details
virtual void setLabel (const std::string& label, bool important);
virtual void setLabel (const std::string& label, bool important, bool center);
virtual void loadingOn(bool visible=true);
virtual void loadingOff();
virtual void setProgressRange (size_t range);

@ -51,7 +51,7 @@ void MerchantRepair::setPtr(const MWWorld::Ptr &actor)
{
int maxDurability = iter->getClass().getItemMaxHealth(*iter);
int durability = iter->getClass().getItemHealth(*iter);
if (maxDurability == durability)
if (maxDurability == durability || maxDurability == 0)
continue;
int basePrice = iter->getClass().getValue(*iter);

@ -35,6 +35,11 @@ namespace MWGui
}
}
int MessageBoxManager::getMessagesCount()
{
return mMessageBoxes.size();
}
void MessageBoxManager::clear()
{
if (mInterMessageBoxe)

@ -28,6 +28,8 @@ namespace MWGui
bool createInteractiveMessageBox (const std::string& message, const std::vector<std::string>& buttons);
bool isInteractiveMessageBox ();
int getMessagesCount();
const InteractiveMessageBox* getInteractiveMessageBox() const { return mInterMessageBoxe; }
/// Remove all message boxes

@ -20,6 +20,7 @@
#include "../mwbase/world.hpp"
#include "../mwbase/soundmanager.hpp"
#include "../mwbase/inputmanager.hpp"
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/windowmanager.hpp"
#include "confirmationdialog.hpp"
@ -437,6 +438,7 @@ namespace MWGui
MWBase::Environment::get().getSoundManager()->processChangedSettings(changed);
MWBase::Environment::get().getWindowManager()->processChangedSettings(changed);
MWBase::Environment::get().getInputManager()->processChangedSettings(changed);
MWBase::Environment::get().getMechanicsManager()->processChangedSettings(changed);
}
void SettingsWindow::onKeyboardSwitchClicked(MyGUI::Widget* _sender)

@ -91,8 +91,7 @@ namespace
if (ench->mData.mType == ESM::Enchantment::ConstantEffect)
leftChargePercent = 101;
else
leftChargePercent = (left.mBase.getCellRef().getEnchantmentCharge() == -1) ? 100
: static_cast<int>(left.mBase.getCellRef().getEnchantmentCharge() / static_cast<float>(ench->mData.mCharge) * 100);
leftChargePercent = static_cast<int>(left.mBase.getCellRef().getNormalizedEnchantmentCharge(ench->mData.mCharge) * 100);
}
}
@ -104,8 +103,7 @@ namespace
if (ench->mData.mType == ESM::Enchantment::ConstantEffect)
rightChargePercent = 101;
else
rightChargePercent = (right.mBase.getCellRef().getEnchantmentCharge() == -1) ? 100
: static_cast<int>(right.mBase.getCellRef().getEnchantmentCharge() / static_cast<float>(ench->mData.mCharge) * 100);
rightChargePercent = static_cast<int>(right.mBase.getCellRef().getNormalizedEnchantmentCharge(ench->mData.mCharge) * 100);
}
}

@ -35,8 +35,7 @@ namespace
float price = static_cast<float>(item.getClass().getValue(item));
if (item.getClass().hasItemHealth(item))
{
price *= item.getClass().getItemHealth(item);
price /= item.getClass().getItemMaxHealth(item);
price *= item.getClass().getItemNormalizedHealth(item);
}
return static_cast<int>(price * count);
}

@ -1369,8 +1369,7 @@ namespace MWGui
const ESM::Enchantment* ench = mStore->get<ESM::Enchantment>()
.find(item.getClass().getEnchantment(item));
int chargePercent = (item.getCellRef().getEnchantmentCharge() == -1) ? 100
: static_cast<int>(item.getCellRef().getEnchantmentCharge() / static_cast<float>(ench->mData.mCharge) * 100);
int chargePercent = static_cast<int>(item.getCellRef().getNormalizedEnchantmentCharge(ench->mData.mCharge) * 100);
mHud->setSelectedEnchantItem(item, chargePercent);
mSpellWindow->setTitle(item.getClass().getName(item));
}
@ -1386,7 +1385,7 @@ namespace MWGui
int durabilityPercent = 100;
if (item.getClass().hasItemHealth(item))
{
durabilityPercent = static_cast<int>(item.getClass().getItemHealth(item) / static_cast<float>(item.getClass().getItemMaxHealth(item)) * 100);
durabilityPercent = static_cast<int>(item.getClass().getItemNormalizedHealth(item) * 100);
}
mHud->setSelectedWeapon(item, durabilityPercent);
mInventoryWindow->setTitle(item.getClass().getName(item));
@ -1671,6 +1670,15 @@ namespace MWGui
mHud->setEnemy(enemy);
}
int WindowManager::getMessagesCount() const
{
int count = 0;
if (mMessageBoxManager)
count = mMessageBoxManager->getMessagesCount();
return count;
}
Loading::Listener* WindowManager::getLoadingScreen()
{
return mLoadingScreen;

@ -316,6 +316,8 @@ namespace MWGui
virtual void setEnemy (const MWWorld::Ptr& enemy);
virtual int getMessagesCount() const;
virtual const Translation::Storage& getTranslationDataStorage() const;
void onSoulgemDialogButtonPressed (int button);

@ -147,9 +147,6 @@ void getRestorationPerHourOfSleep (const MWWorld::Ptr& ptr, float& health, float
namespace MWMechanics
{
const float aiProcessingDistance = 7168;
const float sqrAiProcessingDistance = aiProcessingDistance*aiProcessingDistance;
class SoulTrap : public MWMechanics::EffectSourceVisitor
{
MWWorld::Ptr mCreature;
@ -364,7 +361,8 @@ namespace MWMechanics
const ESM::Position& actor1Pos = actor1.getRefData().getPosition();
const ESM::Position& actor2Pos = actor2.getRefData().getPosition();
float sqrDist = (actor1Pos.asVec3() - actor2Pos.asVec3()).length2();
if (sqrDist > sqrAiProcessingDistance)
if (sqrDist > mActorsProcessingRange*mActorsProcessingRange)
return;
// No combat for totally static creatures
@ -1130,8 +1128,11 @@ namespace MWMechanics
}
}
Actors::Actors() {
Actors::Actors()
{
mTimerDisposeSummonsCorpses = 0.2f; // We should add a delay between summoned creature death and its corpse despawning
updateProcessingRange();
}
Actors::~Actors()
@ -1139,6 +1140,23 @@ namespace MWMechanics
clear();
}
float Actors::getProcessingRange() const
{
return mActorsProcessingRange;
}
void Actors::updateProcessingRange()
{
// We have to cap it since using high values (larger than 7168) will make some quests harder or impossible to complete (bug #1876)
static const float maxProcessingRange = 7168.f;
static const float minProcessingRange = maxProcessingRange / 2.f;
float actorsProcessingRange = Settings::Manager::getFloat("actors processing range", "Game");
actorsProcessingRange = std::min(actorsProcessingRange, maxProcessingRange);
actorsProcessingRange = std::max(actorsProcessingRange, minProcessingRange);
mActorsProcessingRange = actorsProcessingRange;
}
void Actors::addActor (const MWWorld::Ptr& ptr, bool updateImmediately)
{
removeActor(ptr);
@ -1184,7 +1202,7 @@ namespace MWMechanics
// Otherwise check if any actor in AI processing range sees the target actor
std::vector<MWWorld::Ptr> actors;
osg::Vec3f position (actor.getRefData().getPosition().asVec3());
getObjectsInRange(position, aiProcessingDistance, actors);
getObjectsInRange(position, mActorsProcessingRange, actors);
for(std::vector<MWWorld::Ptr>::iterator it = actors.begin(); it != actors.end(); ++it)
{
if (*it == actor)
@ -1242,7 +1260,7 @@ namespace MWMechanics
{
if (iter->first == player) continue;
bool inProcessingRange = (playerPos - iter->first.getRefData().getPosition().asVec3()).length2() <= sqrAiProcessingDistance;
bool inProcessingRange = (playerPos - iter->first.getRefData().getPosition().asVec3()).length2() <= mActorsProcessingRange*mActorsProcessingRange;
if (inProcessingRange)
{
MWMechanics::CreatureStats& stats = iter->first.getClass().getCreatureStats(iter->first);
@ -1288,7 +1306,8 @@ namespace MWMechanics
if (timerUpdateEquippedLight >= updateEquippedLightInterval) timerUpdateEquippedLight = 0;
// show torches only when there are darkness and no precipitations
bool showTorches = MWBase::Environment::get().getWorld()->useTorches();
MWBase::World* world = MWBase::Environment::get().getWorld();
bool showTorches = world->useTorches();
MWWorld::Ptr player = getPlayer();
const osg::Vec3f playerPos = player.getRefData().getPosition().asVec3();
@ -1301,7 +1320,7 @@ namespace MWMechanics
int attackedByPlayerId = player.getClass().getCreatureStats(player).getHitAttemptActorId();
if (attackedByPlayerId != -1)
{
const MWWorld::Ptr playerHitAttemptActor = MWBase::Environment::get().getWorld()->searchPtrViaActorId(attackedByPlayerId);
const MWWorld::Ptr playerHitAttemptActor = world->searchPtrViaActorId(attackedByPlayerId);
if (!playerHitAttemptActor.isInCell())
player.getClass().getCreatureStats(player).setHitAttemptActorId(-1);
@ -1314,14 +1333,11 @@ namespace MWMechanics
CharacterController* ctrl = iter->second->getCharacterController();
float distSqr = (playerPos - iter->first.getRefData().getPosition().asVec3()).length2();
// AI processing is only done within distance of 7168 units to the player. Note the "AI distance" slider doesn't affect this
// (it only does some throttling for targets beyond the "AI distance", so doesn't give any guarantees as to whether AI will be enabled or not)
// This distance could be made configurable later, but the setting must be marked with a big warning:
// using higher values will make a quest in Bloodmoon harder or impossible to complete (bug #1876)
bool inProcessingRange = distSqr <= sqrAiProcessingDistance;
// AI processing is only done within given distance to the player.
bool inProcessingRange = distSqr <= mActorsProcessingRange*mActorsProcessingRange;
if (isPlayer)
ctrl->setAttackingOrSpell(MWBase::Environment::get().getWorld()->getPlayer().getAttackingOrSpell());
ctrl->setAttackingOrSpell(world->getPlayer().getAttackingOrSpell());
// If dead or no longer in combat, no longer store any actors who attempted to hit us. Also remove for the player.
if (iter->first != player && (iter->first.getClass().getCreatureStats(iter->first).isDead()
@ -1335,10 +1351,10 @@ namespace MWMechanics
if (!iter->first.getClass().getCreatureStats(iter->first).isDead())
{
bool cellChanged = MWBase::Environment::get().getWorld()->hasCellChanged();
bool cellChanged = world->hasCellChanged();
MWWorld::Ptr actor = iter->first; // make a copy of the map key to avoid it being invalidated when the player teleports
updateActor(actor, duration);
if (!cellChanged && MWBase::Environment::get().getWorld()->hasCellChanged())
if (!cellChanged && world->hasCellChanged())
{
return; // for now abort update of the old cell when cell changes by teleportation magic effect
// a better solution might be to apply cell changes at the end of the frame
@ -1363,7 +1379,7 @@ namespace MWMechanics
MWWorld::Ptr headTrackTarget;
MWMechanics::CreatureStats& stats = iter->first.getClass().getCreatureStats(iter->first);
bool firstPersonPlayer = isPlayer && MWBase::Environment::get().getWorld()->isFirstPerson();
bool firstPersonPlayer = isPlayer && world->isFirstPerson();
// 1. Unconsious actor can not track target
// 2. Actors in combat and pursue mode do not bother to headtrack
@ -1423,27 +1439,25 @@ namespace MWMechanics
CharacterController* playerCharacter = nullptr;
for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter)
{
const float animationDistance = aiProcessingDistance + 400; // Slightly larger than AI distance so there is time to switch back to the idle animation.
const float distSqr = (playerPos - iter->first.getRefData().getPosition().asVec3()).length2();
const float dist = (playerPos - iter->first.getRefData().getPosition().asVec3()).length();
bool isPlayer = iter->first == player;
bool inAnimationRange = isPlayer || (animationDistance == 0 || distSqr <= animationDistance*animationDistance);
bool inRange = isPlayer || dist <= mActorsProcessingRange;
int activeFlag = 1; // Can be changed back to '2' to keep updating bounding boxes off screen (more accurate, but slower)
if (isPlayer)
activeFlag = 2;
int active = inAnimationRange ? activeFlag : 0;
bool canFly = iter->first.getClass().canFly(iter->first);
if (canFly)
{
// Keep animating flying creatures so they don't just hover in-air
inAnimationRange = true;
active = std::max(1, active);
}
int active = inRange ? activeFlag : 0;
CharacterController* ctrl = iter->second->getCharacterController();
ctrl->setActive(active);
if (!inAnimationRange)
if (!inRange)
{
iter->first.getRefData().getBaseNode()->setNodeMask(0);
world->setActorCollisionMode(iter->first, false);
continue;
}
else if (!isPlayer)
iter->first.getRefData().getBaseNode()->setNodeMask(1<<3);
if (iter->first.getClass().getCreatureStats(iter->first).isParalyzed())
ctrl->skipAnim();
@ -1455,11 +1469,28 @@ namespace MWMechanics
playerCharacter = ctrl;
continue;
}
world->setActorCollisionMode(iter->first, true);
ctrl->update(duration);
// Fade away actors on large distance (>90% of actor's processing distance)
float visibilityRatio = 1.0;
float fadeStartDistance = mActorsProcessingRange*0.9f;
float fadeEndDistance = mActorsProcessingRange;
float fadeRatio = (dist - fadeStartDistance)/(fadeEndDistance - fadeStartDistance);
if (fadeRatio > 0)
visibilityRatio -= std::max(0.f, fadeRatio);
visibilityRatio = std::min(1.f, visibilityRatio);
ctrl->setVisibility(visibilityRatio);
}
if (playerCharacter)
{
playerCharacter->update(duration);
playerCharacter->setVisibility(1.f);
}
for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter)
{
@ -1671,7 +1702,7 @@ namespace MWMechanics
restoreDynamicStats(iter->first, sleep);
if ((!iter->first.getRefData().getBaseNode()) ||
(playerPos - iter->first.getRefData().getPosition().asVec3()).length2() > sqrAiProcessingDistance)
(playerPos - iter->first.getRefData().getPosition().asVec3()).length2() > mActorsProcessingRange*mActorsProcessingRange)
continue;
adjustMagicEffects (iter->first);
@ -1915,7 +1946,7 @@ namespace MWMechanics
std::list<MWWorld::Ptr> list;
std::vector<MWWorld::Ptr> neighbors;
osg::Vec3f position (actor.getRefData().getPosition().asVec3());
getObjectsInRange(position, aiProcessingDistance, neighbors);
getObjectsInRange(position, mActorsProcessingRange, neighbors);
for(auto neighbor = neighbors.begin(); neighbor != neighbors.end(); ++neighbor)
{
if (*neighbor == actor)
@ -1936,7 +1967,7 @@ namespace MWMechanics
std::list<MWWorld::Ptr> list;
std::vector<MWWorld::Ptr> neighbors;
osg::Vec3f position (actor.getRefData().getPosition().asVec3());
getObjectsInRange(position, aiProcessingDistance, neighbors);
getObjectsInRange(position, mActorsProcessingRange, neighbors);
std::set<MWWorld::Ptr> followers;
getActorsFollowing(actor, followers);

@ -65,6 +65,9 @@ namespace MWMechanics
/// paused we may want to do it manually (after equipping permanent enchantment)
void updateMagicEffects (const MWWorld::Ptr& ptr);
void updateProcessingRange();
float getProcessingRange() const;
void addActor (const MWWorld::Ptr& ptr, bool updateImmediately=false);
///< Register an actor for stats management
///
@ -168,6 +171,7 @@ namespace MWMechanics
private:
PtrActorMap mActors;
float mTimerDisposeSummonsCorpses;
float mActorsProcessingRange;
};
}

@ -36,7 +36,7 @@ namespace MWMechanics
return true; //Target doesn't exist
//Set the target destination for the actor
ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos;
const auto dest = target.getRefData().getPosition().asVec3();
if (pathTo(actor, dest, duration, MWBase::Environment::get().getWorld()->getMaxActivationDistance())) //Stop when you get in activation range
{

@ -37,7 +37,7 @@ bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::Charac
if (!target)
return true;
if (!mManual && !pathTo(actor, target.getRefData().getPosition().pos, duration, mDistance))
if (!mManual && !pathTo(actor, target.getRefData().getPosition().asVec3(), duration, mDistance))
{
return false;
}

@ -128,7 +128,7 @@ namespace MWMechanics
float targetReachedTolerance = 0.0f;
if (storage.mLOS)
targetReachedTolerance = storage.mAttackRange;
bool is_target_reached = pathTo(actor, target.getRefData().getPosition().pos, duration, targetReachedTolerance);
const bool is_target_reached = pathTo(actor, target.getRefData().getPosition().asVec3(), duration, targetReachedTolerance);
if (is_target_reached) storage.mReadyToAttack = true;
}
@ -307,7 +307,7 @@ namespace MWMechanics
osg::Vec3f localPos = actor.getRefData().getPosition().asVec3();
coords.toLocal(localPos);
int closestPointIndex = PathFinder::GetClosestPoint(pathgrid, localPos);
int closestPointIndex = PathFinder::getClosestPoint(pathgrid, localPos);
for (int i = 0; i < static_cast<int>(pathgrid->mPoints.size()); i++)
{
if (i != closestPointIndex && getPathGridGraph(storage.mCell).isPointConnected(closestPointIndex, i))
@ -359,7 +359,7 @@ namespace MWMechanics
float dist = (actor.getRefData().getPosition().asVec3() - target.getRefData().getPosition().asVec3()).length();
if ((dist > fFleeDistance && !storage.mLOS)
|| pathTo(actor, storage.mFleeDest, duration))
|| pathTo(actor, PathFinder::makeOsgVec3(storage.mFleeDest), duration))
{
state = AiCombatStorage::FleeState_Idle;
}

@ -100,7 +100,7 @@ namespace MWMechanics
point.mAutogenerated = 0;
point.mConnectionNum = 0;
point.mUnknown = 0;
if (pathTo(actor,point,duration)) //Returns true on path complete
if (pathTo(actor, osg::Vec3f(mX, mY, mZ), duration)) //Returns true on path complete
{
mRemainingDuration = mDuration;
return true;

@ -163,7 +163,7 @@ bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characte
}
//Set the target destination from the actor
ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos;
const auto dest = target.getRefData().getPosition().asVec3();
short baseFollowDistance = followDistance;
short threshold = 30; // to avoid constant switching between moving/stopping
@ -196,7 +196,7 @@ bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characte
if (storage.mMoving)
{
//Check if you're far away
float dist = distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]);
float dist = distance(dest, pos.asVec3());
if (dist > 450)
actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, true); //Make NPC run

@ -5,6 +5,7 @@
#include <components/esm/loadcell.hpp>
#include <components/esm/loadland.hpp>
#include <components/esm/loadmgef.hpp>
#include <components/detournavigator/navigator.hpp>
#include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp"
@ -90,70 +91,67 @@ void MWMechanics::AiPackage::reset()
mTimer = AI_REACTION_TIME + 1.0f;
mIsShortcutting = false;
mShortcutProhibited = false;
mShortcutFailPos = ESM::Pathgrid::Point();
mShortcutFailPos = osg::Vec3f();
mPathFinder.clearPath();
mObstacleCheck.clear();
}
bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest, float duration, float destTolerance)
bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& dest, float duration, float destTolerance)
{
mTimer += duration; //Update timer
ESM::Position pos = actor.getRefData().getPosition(); //position of the actor
const auto position = actor.getRefData().getPosition().asVec3(); //position of the actor
const auto world = MWBase::Environment::get().getWorld();
{
const auto halfExtents = world->getHalfExtents(actor);
world->updateActorPath(actor, mPathFinder.getPath(), halfExtents, position, dest);
}
/// Stops the actor when it gets too close to a unloaded cell
//... At current time, this test is unnecessary. AI shuts down when actor is more than 7168
//... At current time, this test is unnecessary. AI shuts down when actor is more than "actors processing range" setting value
//... units from player, and exterior cells are 8192 units long and wide.
//... But AI processing distance may increase in the future.
if (isNearInactiveCell(pos))
if (isNearInactiveCell(position))
{
actor.getClass().getMovementSettings(actor).mPosition[1] = 0;
return false;
}
// handle path building and shortcutting
ESM::Pathgrid::Point start = pos.pos;
float distToTarget = distance(start, dest);
bool isDestReached = (distToTarget <= destTolerance);
const float distToTarget = distance(position, dest);
const bool isDestReached = (distToTarget <= destTolerance);
if (!isDestReached && mTimer > AI_REACTION_TIME)
{
if (actor.getClass().isBipedal(actor))
openDoors(actor);
bool wasShortcutting = mIsShortcutting;
const bool wasShortcutting = mIsShortcutting;
bool destInLOS = false;
const MWWorld::Class& actorClass = actor.getClass();
MWBase::World* world = MWBase::Environment::get().getWorld();
// check if actor can move along z-axis
bool actorCanMoveByZ = (actorClass.canSwim(actor) && MWBase::Environment::get().getWorld()->isSwimming(actor))
|| world->isFlying(actor);
const bool actorCanMoveByZ = canActorMoveByZAxis(actor);
// Prohibit shortcuts for AiWander, if the actor can not move in 3 dimensions.
if (actorCanMoveByZ || getTypeId() != TypeIdWander)
mIsShortcutting = shortcutPath(start, dest, actor, &destInLOS, actorCanMoveByZ); // try to shortcut first
mIsShortcutting = actorCanMoveByZ
&& shortcutPath(position, dest, actor, &destInLOS, actorCanMoveByZ); // try to shortcut first
if (!mIsShortcutting)
{
if (wasShortcutting || doesPathNeedRecalc(dest, actor.getCell())) // if need to rebuild path
{
mPathFinder.buildSyncedPath(start, dest, actor.getCell(), getPathGridGraph(actor.getCell()));
const auto playerHalfExtents = world->getHalfExtents(world->getPlayerPtr());
mPathFinder.buildPath(actor, position, dest, actor.getCell(), getPathGridGraph(actor.getCell()),
playerHalfExtents, getNavigatorFlags(actor));
mRotateOnTheRunChecks = 3;
// give priority to go directly on target if there is minimal opportunity
if (destInLOS && mPathFinder.getPath().size() > 1)
{
// get point just before dest
std::list<ESM::Pathgrid::Point>::const_iterator pPointBeforeDest = mPathFinder.getPath().end();
--pPointBeforeDest;
--pPointBeforeDest;
auto pPointBeforeDest = mPathFinder.getPath().rbegin() + 1;
// if start point is closer to the target then last point of path (excluding target itself) then go straight on the target
if (distance(start, dest) <= distance(dest, *pPointBeforeDest))
if (distance(position, dest) <= distance(dest, *pPointBeforeDest))
{
mPathFinder.clearPath();
mPathFinder.addPointToPath(dest);
@ -163,7 +161,7 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgr
if (!mPathFinder.getPath().empty()) //Path has points in it
{
ESM::Pathgrid::Point lastPos = mPathFinder.getPath().back(); //Get the end of the proposed path
const auto& lastPos = mPathFinder.getPath().back(); //Get the end of the proposed path
if(distance(dest, lastPos) > 100) //End of the path is far from the destination
mPathFinder.addPointToPath(dest); //Adds the final destination to the path, to try to get to where you want to go
@ -173,15 +171,18 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgr
mTimer = 0;
}
if (isDestReached || mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1])) // if path is finished
const auto pointTolerance = std::min(actor.getClass().getSpeed(actor), DEFAULT_TOLERANCE);
mPathFinder.update(position, pointTolerance, destTolerance);
if (isDestReached || mPathFinder.checkPathCompleted()) // if path is finished
{
// turn to destination point
zTurn(actor, getZAngleToPoint(start, dest));
smoothTurn(actor, getXAngleToPoint(start, dest), 0);
zTurn(actor, getZAngleToPoint(position, dest));
smoothTurn(actor, getXAngleToPoint(position, dest), 0);
return true;
}
else
{
if (mRotateOnTheRunChecks == 0
|| isReachableRotatingOnTheRun(actor, *mPathFinder.getPath().begin())) // to prevent circling around a path point
{
@ -189,25 +190,22 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgr
if (mRotateOnTheRunChecks > 0) mRotateOnTheRunChecks--;
}
// handle obstacles on the way
evadeObstacles(actor, duration, pos);
}
// turn to next path point by X,Z axes
zTurn(actor, mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]));
smoothTurn(actor, mPathFinder.getXAngleToNext(pos.pos[0], pos.pos[1], pos.pos[2]), 0);
zTurn(actor, mPathFinder.getZAngleToNext(position.x(), position.y()));
smoothTurn(actor, mPathFinder.getXAngleToNext(position.x(), position.y(), position.z()), 0);
mObstacleCheck.update(actor, duration);
// handle obstacles on the way
evadeObstacles(actor);
return false;
}
void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor, float duration, const ESM::Position& pos)
void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor)
{
zTurn(actor, mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]));
MWMechanics::Movement& movement = actor.getClass().getMovementSettings(actor);
// check if stuck due to obstacles
if (!mObstacleCheck.check(actor, duration)) return;
if (!mObstacleCheck.isEvading()) return;
// first check if obstacle is a door
static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance();
@ -219,13 +217,14 @@ void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor, float dur
}
else
{
mObstacleCheck.takeEvasiveAction(movement);
mObstacleCheck.takeEvasiveAction(actor.getClass().getMovementSettings(actor));
}
}
void MWMechanics::AiPackage::openDoors(const MWWorld::Ptr& actor)
{
static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance();
const auto world = MWBase::Environment::get().getWorld();
static float distance = world->getMaxActivationDistance();
const MWWorld::Ptr door = getNearbyDoor(actor, distance);
if (door == MWWorld::Ptr())
@ -236,7 +235,7 @@ void MWMechanics::AiPackage::openDoors(const MWWorld::Ptr& actor)
{
if ((door.getCellRef().getTrap().empty() && door.getCellRef().getLockLevel() <= 0 ))
{
MWBase::Environment::get().getWorld()->activate(door, actor);
world->activate(door, actor);
return;
}
@ -248,7 +247,7 @@ void MWMechanics::AiPackage::openDoors(const MWWorld::Ptr& actor)
MWWorld::Ptr keyPtr = invStore.search(keyId);
if (!keyPtr.isEmpty())
MWBase::Environment::get().getWorld()->activate(door, actor);
world->activate(door, actor);
}
}
@ -266,14 +265,15 @@ const MWMechanics::PathgridGraph& MWMechanics::AiPackage::getPathGridGraph(const
return *cache[id].get();
}
bool MWMechanics::AiPackage::shortcutPath(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor, bool *destInLOS, bool isPathClear)
bool MWMechanics::AiPackage::shortcutPath(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::Ptr& actor, bool *destInLOS, bool isPathClear)
{
if (!mShortcutProhibited || (PathFinder::MakeOsgVec3(mShortcutFailPos) - PathFinder::MakeOsgVec3(startPoint)).length() >= PATHFIND_SHORTCUT_RETRY_DIST)
if (!mShortcutProhibited || (mShortcutFailPos - startPoint).length() >= PATHFIND_SHORTCUT_RETRY_DIST)
{
// check if target is clearly visible
isPathClear = !MWBase::Environment::get().getWorld()->castRay(
static_cast<float>(startPoint.mX), static_cast<float>(startPoint.mY), static_cast<float>(startPoint.mZ),
static_cast<float>(endPoint.mX), static_cast<float>(endPoint.mY), static_cast<float>(endPoint.mZ));
startPoint.x(), startPoint.y(), startPoint.z(),
endPoint.x(), endPoint.y(), endPoint.z());
if (destInLOS != nullptr) *destInLOS = isPathClear;
@ -294,21 +294,22 @@ bool MWMechanics::AiPackage::shortcutPath(const ESM::Pathgrid::Point& startPoint
return false;
}
bool MWMechanics::AiPackage::checkWayIsClearForActor(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor)
bool MWMechanics::AiPackage::checkWayIsClearForActor(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::Ptr& actor)
{
bool actorCanMoveByZ = (actor.getClass().canSwim(actor) && MWBase::Environment::get().getWorld()->isSwimming(actor))
|| MWBase::Environment::get().getWorld()->isFlying(actor);
const auto world = MWBase::Environment::get().getWorld();
const bool actorCanMoveByZ = (actor.getClass().canSwim(actor) && world->isSwimming(actor))
|| world->isFlying(actor);
if (actorCanMoveByZ)
return true;
float actorSpeed = actor.getClass().getSpeed(actor);
float maxAvoidDist = AI_REACTION_TIME * actorSpeed + actorSpeed / MAX_VEL_ANGULAR_RADIANS * 2; // *2 - for reliability
osg::Vec3f::value_type distToTarget = osg::Vec3f(static_cast<float>(endPoint.mX), static_cast<float>(endPoint.mY), 0).length();
const float actorSpeed = actor.getClass().getSpeed(actor);
const float maxAvoidDist = AI_REACTION_TIME * actorSpeed + actorSpeed / MAX_VEL_ANGULAR_RADIANS * 2; // *2 - for reliability
const float distToTarget = osg::Vec2f(endPoint.x(), endPoint.y()).length();
float offsetXY = distToTarget > maxAvoidDist*1.5? maxAvoidDist : maxAvoidDist/2;
const float offsetXY = distToTarget > maxAvoidDist*1.5? maxAvoidDist : maxAvoidDist/2;
bool isClear = checkWayIsClear(PathFinder::MakeOsgVec3(startPoint), PathFinder::MakeOsgVec3(endPoint), offsetXY);
const bool isClear = checkWayIsClear(startPoint, endPoint, offsetXY);
// update shortcut prohibit state
if (isClear)
@ -316,12 +317,12 @@ bool MWMechanics::AiPackage::checkWayIsClearForActor(const ESM::Pathgrid::Point&
if (mShortcutProhibited)
{
mShortcutProhibited = false;
mShortcutFailPos = ESM::Pathgrid::Point();
mShortcutFailPos = osg::Vec3f();
}
}
if (!isClear)
{
if (mShortcutFailPos.mX == 0 && mShortcutFailPos.mY == 0 && mShortcutFailPos.mZ == 0)
if (mShortcutFailPos == osg::Vec3f())
{
mShortcutProhibited = true;
mShortcutFailPos = startPoint;
@ -331,9 +332,11 @@ bool MWMechanics::AiPackage::checkWayIsClearForActor(const ESM::Pathgrid::Point&
return isClear;
}
bool MWMechanics::AiPackage::doesPathNeedRecalc(const ESM::Pathgrid::Point& newDest, const MWWorld::CellStore* currentCell)
bool MWMechanics::AiPackage::doesPathNeedRecalc(const osg::Vec3f& newDest, const MWWorld::CellStore* currentCell)
{
return mPathFinder.getPath().empty() || (distance(mPathFinder.getPath().back(), newDest) > 10) || mPathFinder.getPathCell() != currentCell;
return mPathFinder.getPath().empty()
|| (distance(mPathFinder.getPath().back(), newDest) > 10)
|| mPathFinder.getPathCell() != currentCell;
}
bool MWMechanics::AiPackage::isTargetMagicallyHidden(const MWWorld::Ptr& target)
@ -343,23 +346,22 @@ bool MWMechanics::AiPackage::isTargetMagicallyHidden(const MWWorld::Ptr& target)
|| (magicEffects.get(ESM::MagicEffect::Chameleon).getMagnitude() > 75);
}
bool MWMechanics::AiPackage::isNearInactiveCell(const ESM::Position& actorPos)
bool MWMechanics::AiPackage::isNearInactiveCell(osg::Vec3f position)
{
const ESM::Cell* playerCell(getPlayer().getCell()->getCell());
if (playerCell->isExterior())
{
// get actor's distance from origin of center cell
osg::Vec3f actorOffset(actorPos.asVec3());
CoordinateConverter(playerCell).toLocal(actorOffset);
CoordinateConverter(playerCell).toLocal(position);
// currently assumes 3 x 3 grid for exterior cells, with player at center cell.
// ToDo: (Maybe) use "exterior cell load distance" setting to get count of actual active cells
// While AI Process distance is 7168, AI shuts down actors before they reach edges of 3 x 3 grid.
// AI shuts down actors before they reach edges of 3 x 3 grid.
const float distanceFromEdge = 200.0;
float minThreshold = (-1.0f * ESM::Land::REAL_SIZE) + distanceFromEdge;
float maxThreshold = (2.0f * ESM::Land::REAL_SIZE) - distanceFromEdge;
return (actorOffset[0] < minThreshold) || (maxThreshold < actorOffset[0])
|| (actorOffset[1] < minThreshold) || (maxThreshold < actorOffset[1]);
return (position.x() < minThreshold) || (maxThreshold < position.x())
|| (position.y() < minThreshold) || (maxThreshold < position.y());
}
else
{
@ -367,7 +369,7 @@ bool MWMechanics::AiPackage::isNearInactiveCell(const ESM::Position& actorPos)
}
}
bool MWMechanics::AiPackage::isReachableRotatingOnTheRun(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest)
bool MWMechanics::AiPackage::isReachableRotatingOnTheRun(const MWWorld::Ptr& actor, const osg::Vec3f& dest)
{
// get actor's shortest radius for moving in circle
float speed = actor.getClass().getSpeed(actor);
@ -383,15 +385,38 @@ bool MWMechanics::AiPackage::isReachableRotatingOnTheRun(const MWWorld::Ptr& act
radiusDir *= radius;
// pick up the nearest center candidate
osg::Vec3f dest_ = PathFinder::MakeOsgVec3(dest);
osg::Vec3f pos = actor.getRefData().getPosition().asVec3();
osg::Vec3f center1 = pos - radiusDir;
osg::Vec3f center2 = pos + radiusDir;
osg::Vec3f center = (center1 - dest_).length2() < (center2 - dest_).length2() ? center1 : center2;
osg::Vec3f center = (center1 - dest).length2() < (center2 - dest).length2() ? center1 : center2;
float distToDest = (center - dest_).length();
float distToDest = (center - dest).length();
// if pathpoint is reachable for the actor rotating on the run:
// no points of actor's circle should be farther from the center than destination point
return (radius <= distToDest);
}
DetourNavigator::Flags MWMechanics::AiPackage::getNavigatorFlags(const MWWorld::Ptr& actor) const
{
const auto& actorClass = actor.getClass();
DetourNavigator::Flags result = DetourNavigator::Flag_none;
if (actorClass.isPureWaterCreature(actor) || (getTypeId() != TypeIdWander && actorClass.canSwim(actor)))
result |= DetourNavigator::Flag_swim;
if (actorClass.canWalk(actor))
result |= DetourNavigator::Flag_walk;
if (actorClass.isBipedal(actor) && getTypeId() != TypeIdWander)
result |= DetourNavigator::Flag_openDoor;
return result;
}
bool MWMechanics::AiPackage::canActorMoveByZAxis(const MWWorld::Ptr& actor) const
{
const auto world = MWBase::Environment::get().getWorld();
const auto& actorClass = actor.getClass();
return (actorClass.canSwim(actor) && world->isSwimming(actor)) || world->isFlying(actor);
}

@ -7,6 +7,8 @@
#include "obstacle.hpp"
#include "aistate.hpp"
#include <boost/optional.hpp>
namespace MWWorld
{
class Ptr;
@ -105,29 +107,35 @@ namespace MWMechanics
bool isTargetMagicallyHidden(const MWWorld::Ptr& target);
/// Return if actor's rotation speed is sufficient to rotate to the destination pathpoint on the run. Otherwise actor should rotate while standing.
static bool isReachableRotatingOnTheRun(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest);
static bool isReachableRotatingOnTheRun(const MWWorld::Ptr& actor, const osg::Vec3f& dest);
protected:
/// Handles path building and shortcutting with obstacles avoiding
/** \return If the actor has arrived at his destination **/
bool pathTo(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest, float duration, float destTolerance = 0.0f);
bool pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& dest, float duration, float destTolerance = 0.0f);
/// Check if there aren't any obstacles along the path to make shortcut possible
/// If a shortcut is possible then path will be cleared and filled with the destination point.
/// \param destInLOS If not nullptr function will return ray cast check result
/// \return If can shortcut the path
bool shortcutPath(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor, bool *destInLOS, bool isPathClear);
bool shortcutPath(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::Ptr& actor,
bool *destInLOS, bool isPathClear);
/// Check if the way to the destination is clear, taking into account actor speed
bool checkWayIsClearForActor(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor);
bool checkWayIsClearForActor(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::Ptr& actor);
bool doesPathNeedRecalc(const osg::Vec3f& newDest, const MWWorld::CellStore* currentCell);
virtual bool doesPathNeedRecalc(const ESM::Pathgrid::Point& newDest, const MWWorld::CellStore* currentCell);
void evadeObstacles(const MWWorld::Ptr& actor);
void evadeObstacles(const MWWorld::Ptr& actor, float duration, const ESM::Position& pos);
void openDoors(const MWWorld::Ptr& actor);
const PathgridGraph& getPathGridGraph(const MWWorld::CellStore* cell);
DetourNavigator::Flags getNavigatorFlags(const MWWorld::Ptr& actor) const;
bool canActorMoveByZAxis(const MWWorld::Ptr& actor) const;
// TODO: all this does not belong here, move into temporary storage
PathFinder mPathFinder;
ObstacleCheck mObstacleCheck;
@ -137,16 +145,14 @@ namespace MWMechanics
std::string mTargetActorRefId;
mutable int mTargetActorId;
osg::Vec3f mLastActorPos;
short mRotateOnTheRunChecks; // attempts to check rotation to the pathpoint on the run possibility
bool mIsShortcutting; // if shortcutting at the moment
bool mShortcutProhibited; // shortcutting may be prohibited after unsuccessful attempt
ESM::Pathgrid::Point mShortcutFailPos; // position of last shortcut fail
osg::Vec3f mShortcutFailPos; // position of last shortcut fail
private:
bool isNearInactiveCell(const ESM::Position& actorPos);
bool isNearInactiveCell(osg::Vec3f position);
};
}

@ -49,13 +49,13 @@ bool AiPursue::execute (const MWWorld::Ptr& actor, CharacterController& characte
actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing);
//Set the target desition from the actor
ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos;
const auto dest = target.getRefData().getPosition().asVec3();
ESM::Position aPos = actor.getRefData().getPosition();
float pathTolerance = 100.0;
if (pathTo(actor, dest, duration, pathTolerance) &&
std::abs(dest.mZ - aPos.pos[2]) < pathTolerance) // check the true distance in case the target is far away in Z-direction
std::abs(dest.z() - aPos.pos[2]) < pathTolerance) // check the true distance in case the target is far away in Z-direction
{
target.getClass().activate(target,actor).get()->execute(actor); //Arrest player when reached
return true;

@ -3,8 +3,9 @@
#include <components/esm/aisequence.hpp>
#include <components/esm/loadcell.hpp>
#include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/world.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/cellstore.hpp"
@ -21,7 +22,8 @@ bool isWithinMaxRange(const osg::Vec3f& pos1, const osg::Vec3f& pos2)
// Maximum travel distance for vanilla compatibility.
// Was likely meant to prevent NPCs walking into non-loaded exterior cells, but for some reason is used in interior cells as well.
// We can make this configurable at some point, but the default *must* be the below value. Anything else will break shoddily-written content (*cough* MW *cough*) in bizarre ways.
return (pos1 - pos2).length2() <= 7168*7168;
bool aiDistance = MWBase::Environment::get().getMechanicsManager()->getActorsProcessingRange();
return (pos1 - pos2).length2() <= aiDistance*aiDistance;
}
}
@ -56,8 +58,7 @@ namespace MWMechanics
// Unfortunately, with vanilla assets destination is sometimes blocked by other actor.
// If we got close to target, check for actors nearby. If they are, finish AI package.
int destinationTolerance = 64;
ESM::Pathgrid::Point dest(static_cast<int>(mX), static_cast<int>(mY), static_cast<int>(mZ));
if (distance(pos.pos, dest) <= destinationTolerance)
if (distance(pos.asVec3(), osg::Vec3f(mX, mY, mZ)) <= destinationTolerance)
{
std::vector<MWWorld::Ptr> targetActors;
std::pair<MWWorld::Ptr, osg::Vec3f> result = MWBase::Environment::get().getWorld()->getHitContact(actor, destinationTolerance, targetActors);
@ -69,7 +70,7 @@ namespace MWMechanics
}
}
if (pathTo(actor, dest, duration))
if (pathTo(actor, osg::Vec3f(mX, mY, mZ), duration))
{
actor.getClass().getMovementSettings(actor).mPosition[1] = 0;
return true;

@ -50,7 +50,8 @@ namespace MWMechanics
AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector<unsigned char>& idle, bool repeat):
mDistance(distance), mDuration(duration), mRemainingDuration(duration), mTimeOfDay(timeOfDay), mIdle(idle),
mRepeat(repeat), mStoredInitialActorPosition(false), mInitialActorPosition(osg::Vec3f(0, 0, 0)), mHasDestination(false), mDestination(osg::Vec3f(0, 0, 0))
mRepeat(repeat), mStoredInitialActorPosition(false), mInitialActorPosition(osg::Vec3f(0, 0, 0)),
mHasDestination(false), mDestination(osg::Vec3f(0, 0, 0)), mUsePathgrid(false)
{
mIdle.resize(8, 0);
init();
@ -151,16 +152,24 @@ namespace MWMechanics
// rebuild a path to it
if (!mPathFinder.isPathConstructed() && mHasDestination)
{
ESM::Pathgrid::Point dest(PathFinder::MakePathgridPoint(mDestination));
ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(pos));
mPathFinder.buildSyncedPath(start, dest, actor.getCell(), getPathGridGraph(actor.getCell()));
if (mUsePathgrid)
{
mPathFinder.buildPathByPathgrid(pos.asVec3(), mDestination, actor.getCell(),
getPathGridGraph(actor.getCell()));
}
else
{
const auto world = MWBase::Environment::get().getWorld();
const auto playerHalfExtents = world->getHalfExtents(world->getPlayerPtr());
mPathFinder.buildPath(actor, pos.asVec3(), mDestination, actor.getCell(),
getPathGridGraph(actor.getCell()), playerHalfExtents, getNavigatorFlags(actor));
}
if (mPathFinder.isPathConstructed())
storage.setState(AiWanderStorage::Wander_Walking);
}
doPerFrameActionsForState(actor, duration, storage, pos);
doPerFrameActionsForState(actor, duration, storage);
playIdleDialogueRandomly(actor);
@ -169,14 +178,14 @@ namespace MWMechanics
if (AI_REACTION_TIME <= lastReaction)
{
lastReaction = 0;
return reactionTimeActions(actor, storage, currentCell, cellChange, pos, duration);
return reactionTimeActions(actor, storage, currentCell, cellChange, pos);
}
else
return false;
}
bool AiWander::reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage,
const MWWorld::CellStore*& currentCell, bool cellChange, ESM::Position& pos, float duration)
const MWWorld::CellStore*& currentCell, bool cellChange, ESM::Position& pos)
{
if (mDistance <= 0)
storage.mCanWanderAlongPathGrid = false;
@ -226,7 +235,7 @@ namespace MWMechanics
}
// If Wandering manually and hit an obstacle, stop
if (storage.mIsWanderingManually && mObstacleCheck.check(actor, duration, 2.0f)) {
if (storage.mIsWanderingManually && mObstacleCheck.isEvading()) {
completeManualWalking(actor, storage);
}
@ -251,7 +260,9 @@ namespace MWMechanics
setPathToAnAllowedNode(actor, storage, pos);
}
}
} else if (storage.mIsWanderingManually && mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], DESTINATION_TOLERANCE)) {
}
else if (storage.mIsWanderingManually && mPathFinder.checkPathCompleted())
{
completeManualWalking(actor, storage);
}
@ -292,11 +303,9 @@ namespace MWMechanics
* Commands actor to walk to a random location near original spawn location.
*/
void AiWander::wanderNearStart(const MWWorld::Ptr &actor, AiWanderStorage &storage, int wanderDistance) {
const ESM::Pathgrid::Point currentPosition = actor.getRefData().getPosition().pos;
const osg::Vec3f currentPositionVec3f = osg::Vec3f(currentPosition.mX, currentPosition.mY, currentPosition.mZ);
const auto currentPosition = actor.getRefData().getPosition().asVec3();
std::size_t attempts = 10; // If a unit can't wander out of water, don't want to hang here
ESM::Pathgrid::Point destinationPosition;
bool isWaterCreature = actor.getClass().isPureWaterCreature(actor);
do {
// Determine a random location within radius of original position
@ -305,19 +314,24 @@ namespace MWMechanics
const float destinationX = mInitialActorPosition.x() + wanderRadius * std::cos(randomDirection);
const float destinationY = mInitialActorPosition.y() + wanderRadius * std::sin(randomDirection);
const float destinationZ = mInitialActorPosition.z();
destinationPosition = ESM::Pathgrid::Point(destinationX, destinationY, destinationZ);
const osg::Vec3f destinationPosition(destinationX, destinationY, destinationZ);
mDestination = osg::Vec3f(destinationX, destinationY, destinationZ);
// Check if land creature will walk onto water or if water creature will swim onto land
if ((!isWaterCreature && !destinationIsAtWater(actor, mDestination)) ||
(isWaterCreature && !destinationThroughGround(currentPositionVec3f, mDestination))) {
mPathFinder.buildSyncedPath(currentPosition, destinationPosition, actor.getCell(), getPathGridGraph(actor.getCell()));
(isWaterCreature && !destinationThroughGround(currentPosition, mDestination)))
{
const auto world = MWBase::Environment::get().getWorld();;
const auto playerHalfExtents = world->getHalfExtents(world->getPlayerPtr());
mPathFinder.buildPath(actor, currentPosition, destinationPosition, actor.getCell(),
getPathGridGraph(actor.getCell()), playerHalfExtents, getNavigatorFlags(actor));
mPathFinder.addPointToPath(destinationPosition);
if (mPathFinder.isPathConstructed())
{
storage.setState(AiWanderStorage::Wander_Walking, true);
mHasDestination = true;
mUsePathgrid = false;
}
return;
}
@ -348,7 +362,7 @@ namespace MWMechanics
storage.setState(AiWanderStorage::Wander_IdleNow);
}
void AiWander::doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage, ESM::Position& pos)
void AiWander::doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage)
{
switch (storage.mState)
{
@ -357,7 +371,7 @@ namespace MWMechanics
break;
case AiWanderStorage::Wander_Walking:
onWalkingStatePerFrameActions(actor, duration, storage, pos);
onWalkingStatePerFrameActions(actor, duration, storage);
break;
case AiWanderStorage::Wander_ChooseAction:
@ -413,11 +427,10 @@ namespace MWMechanics
}
}
void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor,
float duration, AiWanderStorage& storage, ESM::Position& pos)
void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage)
{
// Is there no destination or are we there yet?
if ((!mPathFinder.isPathConstructed()) || pathTo(actor, ESM::Pathgrid::Point(mPathFinder.getPath().back()), duration, DESTINATION_TOLERANCE))
if ((!mPathFinder.isPathConstructed()) || pathTo(actor, osg::Vec3f(mPathFinder.getPath().back()), duration, DESTINATION_TOLERANCE))
{
stopWalking(actor, storage);
storage.setState(AiWanderStorage::Wander_ChooseAction);
@ -425,7 +438,7 @@ namespace MWMechanics
else
{
// have not yet reached the destination
evadeObstacles(actor, storage, duration, pos);
evadeObstacles(actor, storage);
}
}
@ -456,7 +469,7 @@ namespace MWMechanics
storage.setState(AiWanderStorage::Wander_IdleNow);
}
void AiWander::evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage, float duration, ESM::Position& pos)
void AiWander::evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage)
{
if (mObstacleCheck.isEvading())
{
@ -592,15 +605,17 @@ namespace MWMechanics
ToWorldCoordinates(dest, storage.mCell->getCell());
// actor position is already in world coordinates
ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(actorPos));
const auto start = actorPos.asVec3();
// don't take shortcuts for wandering
mPathFinder.buildSyncedPath(start, dest, actor.getCell(), getPathGridGraph(actor.getCell()));
const auto destVec3f = PathFinder::makeOsgVec3(dest);
mPathFinder.buildPathByPathgrid(start, destVec3f, actor.getCell(), getPathGridGraph(actor.getCell()));
if (mPathFinder.isPathConstructed())
{
mDestination = osg::Vec3f(dest.mX, dest.mY, dest.mZ);
mDestination = destVec3f;
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);
@ -631,15 +646,15 @@ namespace MWMechanics
// 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::list<ESM::Pathgrid::Point> paths = pathfinder.getPath();
auto paths = pathfinder.getPath();
while(paths.size() >= 2)
{
ESM::Pathgrid::Point pt = paths.back();
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(nodes[j].mX == pt.mX && nodes[j].mY == pt.mY)
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;
@ -731,7 +746,7 @@ namespace MWMechanics
ESM::Pathgrid::Point worldDest = dest;
ToWorldCoordinates(worldDest, actor.getCell()->getCell());
bool isPathGridOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(PathFinder::MakeOsgVec3(worldDest), 60);
bool isPathGridOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(PathFinder::makeOsgVec3(worldDest), 60);
// add offset only if the selected pathgrid is occupied by another actor
if (isPathGridOccupied)
@ -752,18 +767,18 @@ namespace MWMechanics
ESM::Pathgrid::Point connDest = points[randomIndex];
// add an offset towards random neighboring node
osg::Vec3f dir = PathFinder::MakeOsgVec3(connDest) - PathFinder::MakeOsgVec3(dest);
osg::Vec3f dir = PathFinder::makeOsgVec3(connDest) - PathFinder::makeOsgVec3(dest);
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));
dest = PathFinder::makePathgridPoint(PathFinder::makeOsgVec3(dest) + dir * (j * 5 * length / 100.f));
worldDest = dest;
ToWorldCoordinates(worldDest, actor.getCell()->getCell());
isOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(PathFinder::MakeOsgVec3(worldDest), 60);
isOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(PathFinder::makeOsgVec3(worldDest), 60);
if (!isOccupied)
break;
@ -799,7 +814,7 @@ namespace MWMechanics
const ESM::Pathgrid *pathgrid =
MWBase::Environment::get().getWorld()->getStore().get<ESM::Pathgrid>().search(*currentCell->getCell());
int index = PathFinder::GetClosestPoint(pathgrid, PathFinder::MakeOsgVec3(dest));
int index = PathFinder::getClosestPoint(pathgrid, PathFinder::makeOsgVec3(dest));
getPathGridGraph(currentCell).getNeighbouringPoints(index, points);
}
@ -832,7 +847,7 @@ namespace MWMechanics
CoordinateConverter(cell).toLocal(npcPos);
// Find closest pathgrid point
int closestPointIndex = PathFinder::GetClosestPoint(pathgrid, npcPos);
int closestPointIndex = PathFinder::getClosestPoint(pathgrid, npcPos);
// mAllowedNodes for this actor with pathgrid point indexes based on mDistance
// and if the point is connected to the closest current point
@ -840,7 +855,7 @@ namespace MWMechanics
int pointIndex = 0;
for(unsigned int counter = 0; counter < pathgrid->mPoints.size(); counter++)
{
osg::Vec3f nodePos(PathFinder::MakeOsgVec3(pathgrid->mPoints[counter]));
osg::Vec3f nodePos(PathFinder::makeOsgVec3(pathgrid->mPoints[counter]));
if((npcPos - nodePos).length2() <= mDistance * mDistance &&
getPathGridGraph(cellStore).isPointConnected(closestPointIndex, counter))
{
@ -867,7 +882,7 @@ namespace MWMechanics
// 2. Partway along the path between the point and its connected points.
void AiWander::AddNonPathGridAllowedPoints(osg::Vec3f npcPos, const ESM::Pathgrid * pathGrid, int pointIndex, AiWanderStorage& storage)
{
storage.mAllowedNodes.push_back(PathFinder::MakePathgridPoint(npcPos));
storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(npcPos));
for (std::vector<ESM::Pathgrid::Edge>::const_iterator it = pathGrid->mEdges.begin(); it != pathGrid->mEdges.end(); ++it)
{
if (it->mV0 == pointIndex)
@ -879,8 +894,8 @@ namespace MWMechanics
void AiWander::AddPointBetweenPathGridPoints(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 = PathFinder::makeOsgVec3(start);
osg::Vec3f delta = PathFinder::makeOsgVec3(end) - vectorStart;
float length = delta.length();
delta.normalize();
@ -889,7 +904,7 @@ namespace MWMechanics
// must not travel longer than distance between waypoints or NPC goes past waypoint
distance = std::min(distance, static_cast<int>(length));
delta *= distance;
storage.mAllowedNodes.push_back(PathFinder::MakePathgridPoint(vectorStart + delta));
storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(vectorStart + delta));
}
void AiWander::SetCurrentNodeToClosestAllowedNode(const osg::Vec3f& npcPos, AiWanderStorage& storage)
@ -898,7 +913,7 @@ namespace MWMechanics
unsigned int index = 0;
for (unsigned int counterThree = 0; counterThree < storage.mAllowedNodes.size(); counterThree++)
{
osg::Vec3f nodePos(PathFinder::MakeOsgVec3(storage.mAllowedNodes[counterThree]));
osg::Vec3f nodePos(PathFinder::makeOsgVec3(storage.mAllowedNodes[counterThree]));
float tempDist = (npcPos - nodePos).length2();
if (tempDist < distanceToClosestNode)
{

@ -137,15 +137,15 @@ namespace MWMechanics
short unsigned getRandomIdle();
void setPathToAnAllowedNode(const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos);
void playGreetingIfPlayerGetsTooClose(const MWWorld::Ptr& actor, AiWanderStorage& storage);
void evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage, float duration, ESM::Position& pos);
void evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage);
void playIdleDialogueRandomly(const MWWorld::Ptr& actor);
void turnActorToFacePlayer(const osg::Vec3f& actorPosition, const osg::Vec3f& playerPosition, AiWanderStorage& storage);
void doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage, ESM::Position& pos);
void doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage);
void onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage);
void onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage, ESM::Position& pos);
void onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage);
void onChooseActionStatePerFrameActions(const MWWorld::Ptr& actor, AiWanderStorage& storage);
bool reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage,
const MWWorld::CellStore*& currentCell, bool cellChange, ESM::Position& pos, float duration);
const MWWorld::CellStore*& currentCell, bool cellChange, ESM::Position& pos);
bool isPackageCompleted(const MWWorld::Ptr& actor, AiWanderStorage& storage);
void wanderNearStart(const MWWorld::Ptr &actor, AiWanderStorage &storage, int wanderDistance);
bool destinationIsAtWater(const MWWorld::Ptr &actor, const osg::Vec3f& destination);
@ -164,6 +164,7 @@ namespace MWMechanics
bool mHasDestination;
osg::Vec3f mDestination;
bool mUsePathgrid;
void getNeighbouringNodes(ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points);

@ -30,6 +30,7 @@
#include "../mwrender/animation.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/world.hpp"
#include "../mwbase/soundmanager.hpp"
#include "../mwbase/windowmanager.hpp"
@ -1849,7 +1850,7 @@ void CharacterController::updateAnimQueue()
mAnimation->setLoopingEnabled(mAnimQueue.front().mGroup, mAnimQueue.size() <= 1);
}
void CharacterController::update(float duration)
void CharacterController::update(float duration, bool animationOnly)
{
MWBase::World *world = MWBase::Environment::get().getWorld();
const MWWorld::Class &cls = mPtr.getClass();
@ -2235,10 +2236,10 @@ void CharacterController::update(float duration)
world->rotateObject(mPtr, rot.x(), rot.y(), 0.0f, true);
}
if (!mMovementAnimationControlled)
if (!animationOnly && !mMovementAnimationControlled)
world->queueMovement(mPtr, vec);
}
else
else if (!animationOnly)
// We must always queue movement, even if there is none, to apply gravity.
world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f));
@ -2261,6 +2262,7 @@ void CharacterController::update(float duration)
playDeath(1.f, mDeathState);
}
// We must always queue movement, even if there is none, to apply gravity.
if (!animationOnly)
world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f));
}
@ -2295,7 +2297,7 @@ void CharacterController::update(float duration)
moved.z() = 1.0;
// Update movement
if(mMovementAnimationControlled && mPtr.getClass().isActor())
if(!animationOnly && mMovementAnimationControlled && mPtr.getClass().isActor())
world->queueMovement(mPtr, moved);
mSkipAnim = false;
@ -2521,6 +2523,7 @@ void CharacterController::resurrect()
mAnimation->disable(mCurrentDeath);
mCurrentDeath.clear();
mDeathState = CharState_None;
mWeaponType = WeapType_None;
}
void CharacterController::updateContinuousVfx()
@ -2544,6 +2547,19 @@ void CharacterController::updateMagicEffects()
{
if (!mPtr.getClass().isActor())
return;
bool vampire = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude() > 0.0f;
mAnimation->setVampire(vampire);
float light = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Light).getMagnitude();
mAnimation->setLightEffect(light);
}
void CharacterController::setVisibility(float visibility)
{
// We should take actor's invisibility in account
if (mPtr.getClass().isActor())
{
float alpha = 1.f;
if (mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Invisibility).getModifier()) // Ignore base magnitude (see bug #3555).
{
@ -2557,13 +2573,12 @@ void CharacterController::updateMagicEffects()
{
alpha *= std::max(0.2f, (100.f - chameleon)/100.f);
}
mAnimation->setAlpha(alpha);
bool vampire = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude() > 0.0f;
mAnimation->setVampire(vampire);
visibility = std::min(visibility, alpha);
}
float light = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Light).getMagnitude();
mAnimation->setLightEffect(light);
// TODO: implement a dithering shader rather than just change object transparency.
mAnimation->setAlpha(visibility);
}
void CharacterController::setAttackTypeBasedOnMovement()

@ -257,7 +257,7 @@ public:
void updatePtr(const MWWorld::Ptr &ptr);
void update(float duration);
void update(float duration, bool animationOnly=false);
void persistAnimationState();
void unpersistAnimationState();
@ -292,6 +292,7 @@ public:
bool isTurning() const;
bool isAttackingOrSpell() const;
void setVisibility(float visibility);
void setAttackingOrSpell(bool attackingOrSpell);
void castSpell(const std::string spellId, bool manualSpell=false);
void setAIAttackType(const std::string& attackType);

@ -371,11 +371,9 @@ namespace MWMechanics
return;
const bool weaphashealth = weapon.getClass().hasItemHealth(weapon);
if(weaphashealth)
if (weaphashealth)
{
int weaphealth = weapon.getClass().getItemHealth(weapon);
int weapmaxhealth = weapon.getClass().getItemMaxHealth(weapon);
damage *= (float(weaphealth) / weapmaxhealth);
damage *= weapon.getClass().getItemNormalizedHealth(weapon);
}
static const float fDamageStrengthBase = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>()

@ -33,11 +33,12 @@ namespace MWMechanics
point.y() -= static_cast<float>(mCellY);
}
osg::Vec3f CoordinateConverter::toLocalVec3(const ESM::Pathgrid::Point& point)
osg::Vec3f CoordinateConverter::toLocalVec3(const osg::Vec3f& point)
{
return osg::Vec3f(
static_cast<float>(point.mX - mCellX),
static_cast<float>(point.mY - mCellY),
static_cast<float>(point.mZ));
point.x() - static_cast<float>(mCellX),
point.y() - static_cast<float>(mCellY),
point.z()
);
}
}

@ -26,7 +26,7 @@ namespace MWMechanics
/// in-place conversion from world to local
void toLocal(osg::Vec3f& point);
osg::Vec3f toLocalVec3(const ESM::Pathgrid::Point& point);
osg::Vec3f toLocalVec3(const osg::Vec3f& point);
private:
int mCellX;

@ -17,6 +17,7 @@
#include "../mwworld/ptr.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/statemanager.hpp"
#include "../mwbase/world.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwbase/dialoguemanager.hpp"
@ -431,6 +432,29 @@ namespace MWMechanics
mObjects.update(duration, paused);
}
void MechanicsManager::processChangedSettings(const Settings::CategorySettingVector &changed)
{
for (Settings::CategorySettingVector::const_iterator it = changed.begin(); it != changed.end(); ++it)
{
if (it->first == "Game" && it->second == "actors processing range")
{
int state = MWBase::Environment::get().getStateManager()->getState();
if (state != MWBase::StateManager::State_Running)
continue;
mActors.updateProcessingRange();
// Update mechanics for new processing range immediately
update(0.f, false);
}
}
}
float MechanicsManager::getActorsProcessingRange() const
{
return mActors.getProcessingRange();
}
bool MechanicsManager::isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer)
{
return mActors.isActorDetected(actor, observer);

@ -1,6 +1,8 @@
#ifndef GAME_MWMECHANICS_MECHANICSMANAGERIMP_H
#define GAME_MWMECHANICS_MECHANICSMANAGERIMP_H
#include <components/settings/settings.hpp>
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwworld/ptr.hpp"
@ -206,6 +208,10 @@ namespace MWMechanics
virtual void castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell=false);
void processChangedSettings(const Settings::CategorySettingVector& settings) override;
virtual float getActorsProcessingRange() const;
/// Check if the target actor was detected by an observer
/// If the observer is a non-NPC, check all actors in AI processing distance as observers
virtual bool isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer);

@ -96,11 +96,6 @@ namespace MWMechanics
mEvadeDuration = 0;
}
bool ObstacleCheck::isNormalState() const
{
return mWalkState == State_Norm;
}
bool ObstacleCheck::isEvading() const
{
return mWalkState == State_Evade;
@ -128,7 +123,7 @@ namespace MWMechanics
* u = how long to move sideways
*
*/
bool ObstacleCheck::check(const MWWorld::Ptr& actor, float duration, float scaleMinimumDistance)
void ObstacleCheck::update(const MWWorld::Ptr& actor, float duration, float scaleMinimumDistance)
{
const MWWorld::Class& cls = actor.getClass();
ESM::Position pos = actor.getRefData().getPosition();
@ -180,9 +175,7 @@ namespace MWMechanics
case State_Evade:
{
mEvadeDuration += duration;
if(mEvadeDuration < DURATION_TO_EVADE)
return true;
else
if(mEvadeDuration >= DURATION_TO_EVADE)
{
// tried to evade, assume all is ok and start again
mWalkState = State_Norm;
@ -191,10 +184,9 @@ namespace MWMechanics
}
/* NO DEFAULT CASE */
}
return false; // no obstacles to evade (yet)
}
void ObstacleCheck::takeEvasiveAction(MWMechanics::Movement& actorMovement)
void ObstacleCheck::takeEvasiveAction(MWMechanics::Movement& actorMovement) const
{
actorMovement.mPosition[0] = evadeDirections[mEvadeDirectionIndex][0];
actorMovement.mPosition[1] = evadeDirections[mEvadeDirectionIndex][1];

@ -27,15 +27,13 @@ namespace MWMechanics
// Clear the timers and set the state machine to default
void clear();
bool isNormalState() const;
bool isEvading() const;
// Returns true if there is an obstacle and an evasive action
// should be taken
bool check(const MWWorld::Ptr& actor, float duration, float scaleMinimumDistance = 1.0f);
// Updates internal state, call each frame for moving actor
void update(const MWWorld::Ptr& actor, float duration, float scaleMinimumDistance = 1.0f);
// change direction to try to fix "stuck" actor
void takeEvasiveAction(MWMechanics::Movement& actorMovement);
void takeEvasiveAction(MWMechanics::Movement& actorMovement) const;
private:

@ -1,13 +1,20 @@
#include "pathfinding.hpp"
#include <iterator>
#include <limits>
#include <components/detournavigator/exceptions.hpp>
#include <components/detournavigator/debug.hpp>
#include <components/detournavigator/navigator.hpp>
#include <components/debug/debuglog.hpp>
#include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp"
#include "../mwphysics/collisiontype.hpp"
#include "../mwworld/cellstore.hpp"
#include "../mwworld/class.hpp"
#include "pathgrid.hpp"
#include "coordinateconverter.hpp"
@ -29,7 +36,7 @@ namespace
// points to a quadtree may help
for(unsigned int counter = 0; counter < grid->mPoints.size(); counter++)
{
float potentialDistBetween = MWMechanics::PathFinder::DistanceSquared(grid->mPoints[counter], pos);
float potentialDistBetween = MWMechanics::PathFinder::distanceSquared(grid->mPoints[counter], pos);
if (potentialDistBetween < closestDistanceReachable)
{
// found a closer one
@ -57,56 +64,19 @@ namespace
(closestReachableIndex, closestReachableIndex == closestIndex);
}
}
namespace MWMechanics
{
float sqrDistanceIgnoreZ(const ESM::Pathgrid::Point& point, float x, float y)
{
x -= point.mX;
y -= point.mY;
return (x * x + y * y);
}
float distance(const ESM::Pathgrid::Point& point, float x, float y, float z)
{
x -= point.mX;
y -= point.mY;
z -= point.mZ;
return sqrt(x * x + y * y + z * z);
}
float distance(const ESM::Pathgrid::Point& a, const ESM::Pathgrid::Point& b)
{
float x = static_cast<float>(a.mX - b.mX);
float y = static_cast<float>(a.mY - b.mY);
float z = static_cast<float>(a.mZ - b.mZ);
return sqrt(x * x + y * y + z * z);
}
float getZAngleToDir(const osg::Vec3f& dir)
{
return std::atan2(dir.x(), dir.y());
}
float getXAngleToDir(const osg::Vec3f& dir)
float sqrDistance(const osg::Vec2f& lhs, const osg::Vec2f& rhs)
{
float dirLen = dir.length();
return (dirLen != 0) ? -std::asin(dir.z() / dirLen) : 0;
return (lhs - rhs).length2();
}
float getZAngleToPoint(const ESM::Pathgrid::Point &origin, const ESM::Pathgrid::Point &dest)
float sqrDistanceIgnoreZ(const osg::Vec3f& lhs, const osg::Vec3f& rhs)
{
osg::Vec3f dir = PathFinder::MakeOsgVec3(dest) - PathFinder::MakeOsgVec3(origin);
return getZAngleToDir(dir);
}
float getXAngleToPoint(const ESM::Pathgrid::Point &origin, const ESM::Pathgrid::Point &dest)
{
osg::Vec3f dir = PathFinder::MakeOsgVec3(dest) - PathFinder::MakeOsgVec3(origin);
return getXAngleToDir(dir);
return sqrDistance(osg::Vec2f(lhs.x(), lhs.y()), osg::Vec2f(rhs.x(), rhs.y()));
}
}
namespace MWMechanics
{
bool checkWayIsClear(const osg::Vec3f& from, const osg::Vec3f& to, float offsetXY)
{
osg::Vec3f dir = to - from;
@ -121,18 +91,6 @@ namespace MWMechanics
return (std::abs(from.z() - h) <= PATHFIND_Z_REACH);
}
PathFinder::PathFinder()
: mPathgrid(nullptr)
, mCell(nullptr)
{
}
void PathFinder::clearPath()
{
if(!mPath.empty())
mPath.clear();
}
/*
* NOTE: This method may fail to find a path. The caller must check the
* result before using it. If there is no path the AI routies need to
@ -150,7 +108,7 @@ namespace MWMechanics
* pathgrid point (e.g. wander) then it may be worth while to call
* pop_back() to remove the redundant entry.
*
* NOTE: coordinates must be converted prior to calling GetClosestPoint()
* NOTE: coordinates must be converted prior to calling getClosestPoint()
*
* |
* | cell
@ -169,52 +127,44 @@ namespace MWMechanics
* j = @.x in local coordinates (i.e. within the cell)
* k = @.x in world coordinates
*/
void PathFinder::buildPath(const ESM::Pathgrid::Point &startPoint,
const ESM::Pathgrid::Point &endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph)
void PathFinder::buildPathByPathgridImpl(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const PathgridGraph& pathgridGraph, std::back_insert_iterator<std::deque<osg::Vec3f>> out)
{
mPath.clear();
// TODO: consider removing mCell / mPathgrid in favor of mPathgridGraph
if(mCell != cell || !mPathgrid)
{
mCell = cell;
mPathgrid = pathgridGraph.getPathgrid();
}
const auto pathgrid = pathgridGraph.getPathgrid();
// Refer to AiWander reseach topic on openmw forums for some background.
// Maybe there is no pathgrid for this cell. Just go to destination and let
// physics take care of any blockages.
if(!mPathgrid || mPathgrid->mPoints.empty())
if(!pathgrid || pathgrid->mPoints.empty())
{
mPath.push_back(endPoint);
*out++ = endPoint;
return;
}
// NOTE: GetClosestPoint expects local coordinates
// NOTE: getClosestPoint expects local coordinates
CoordinateConverter converter(mCell->getCell());
// NOTE: It is possible that GetClosestPoint returns a pathgrind point index
// NOTE: It is possible that getClosestPoint returns a pathgrind point index
// that is unreachable in some situations. e.g. actor is standing
// outside an area enclosed by walls, but there is a pathgrid
// point right behind the wall that is closer than any pathgrid
// point outside the wall
osg::Vec3f startPointInLocalCoords(converter.toLocalVec3(startPoint));
int startNode = GetClosestPoint(mPathgrid, startPointInLocalCoords);
int startNode = getClosestPoint(pathgrid, startPointInLocalCoords);
osg::Vec3f endPointInLocalCoords(converter.toLocalVec3(endPoint));
std::pair<int, bool> endNode = getClosestReachablePoint(mPathgrid, &pathgridGraph,
std::pair<int, bool> endNode = getClosestReachablePoint(pathgrid, &pathgridGraph,
endPointInLocalCoords,
startNode);
// 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(mPathgrid->mPoints[endNode.first], endPointInLocalCoords);
float startTo1stNodeLength2 = DistanceSquared(mPathgrid->mPoints[startNode], startPointInLocalCoords);
float endTolastNodeLength2 = distanceSquared(pathgrid->mPoints[endNode.first], endPointInLocalCoords);
float startTo1stNodeLength2 = distanceSquared(pathgrid->mPoints[startNode], startPointInLocalCoords);
if ((startToEndLength2 < startTo1stNodeLength2) || (startToEndLength2 < endTolastNodeLength2))
{
mPath.push_back(endPoint);
*out++ = endPoint;
return;
}
@ -225,21 +175,21 @@ namespace MWMechanics
// nodes are the same
if(startNode == endNode.first)
{
ESM::Pathgrid::Point temp(mPathgrid->mPoints[startNode]);
ESM::Pathgrid::Point temp(pathgrid->mPoints[startNode]);
converter.toWorld(temp);
mPath.push_back(temp);
*out++ = makeOsgVec3(temp);
}
else
{
mPath = pathgridGraph.aStarSearch(startNode, endNode.first);
auto path = pathgridGraph.aStarSearch(startNode, endNode.first);
// If nearest path node is in opposite direction from second, remove it from path.
// Especially useful for wandering actors, if the nearest node is blocked for some reason.
if (mPath.size() > 1)
if (path.size() > 1)
{
ESM::Pathgrid::Point secondNode = *(++mPath.begin());
osg::Vec3f firstNodeVec3f = MakeOsgVec3(mPathgrid->mPoints[startNode]);
osg::Vec3f secondNodeVec3f = MakeOsgVec3(secondNode);
ESM::Pathgrid::Point secondNode = *(++path.begin());
osg::Vec3f firstNodeVec3f = makeOsgVec3(pathgrid->mPoints[startNode]);
osg::Vec3f secondNodeVec3f = makeOsgVec3(secondNode);
osg::Vec3f toSecondNodeVec3f = secondNodeVec3f - firstNodeVec3f;
osg::Vec3f toStartPointVec3f = startPointInLocalCoords - firstNodeVec3f;
if (toSecondNodeVec3f * toStartPointVec3f > 0)
@ -248,19 +198,21 @@ namespace MWMechanics
converter.toWorld(temp);
// Add Z offset since path node can overlap with other objects.
// Also ignore doors in raytesting.
int mask = MWPhysics::CollisionType_World;
const int mask = MWPhysics::CollisionType_World;
bool isPathClear = !MWBase::Environment::get().getWorld()->castRay(
startPoint.mX, startPoint.mY, startPoint.mZ+16, temp.mX, temp.mY, temp.mZ+16, mask);
startPoint.x(), startPoint.y(), startPoint.z() + 16, temp.mX, temp.mY, temp.mZ + 16, mask);
if (isPathClear)
mPath.pop_front();
path.pop_front();
}
}
// convert supplied path to world coordinates
for (std::list<ESM::Pathgrid::Point>::iterator iter(mPath.begin()); iter != mPath.end(); ++iter)
std::transform(path.begin(), path.end(), out,
[&] (ESM::Pathgrid::Point& point)
{
converter.toWorld(*iter);
}
converter.toWorld(point);
return makeOsgVec3(point);
});
}
// If endNode found is NOT the closest PathGrid point to the endPoint,
@ -276,7 +228,7 @@ namespace MWMechanics
//
// The AI routines will have to deal with such situations.
if(endNode.second)
mPath.push_back(endPoint);
*out++ = endPoint;
}
float PathFinder::getZAngleToNext(float x, float y) const
@ -286,9 +238,9 @@ namespace MWMechanics
if(mPath.empty())
return 0.;
const ESM::Pathgrid::Point &nextPoint = *mPath.begin();
float directionX = nextPoint.mX - x;
float directionY = nextPoint.mY - y;
const auto& nextPoint = mPath.front();
const float directionX = nextPoint.x() - x;
const float directionY = nextPoint.y() - y;
return std::atan2(directionX, directionY);
}
@ -300,62 +252,64 @@ namespace MWMechanics
if(mPath.empty())
return 0.;
const ESM::Pathgrid::Point &nextPoint = *mPath.begin();
osg::Vec3f dir = MakeOsgVec3(nextPoint) - osg::Vec3f(x,y,z);
const osg::Vec3f dir = mPath.front() - osg::Vec3f(x, y, z);
return getXAngleToDir(dir);
}
bool PathFinder::checkPathCompleted(float x, float y, float tolerance)
void PathFinder::update(const osg::Vec3f& position, const float pointTolerance, const float destinationTolerance)
{
if(mPath.empty())
return true;
if (mPath.empty())
return;
const ESM::Pathgrid::Point& nextPoint = *mPath.begin();
if (sqrDistanceIgnoreZ(nextPoint, x, y) < tolerance*tolerance)
{
const auto tolerance = mPath.size() > 1 ? pointTolerance : destinationTolerance;
if (sqrDistanceIgnoreZ(mPath.front(), position) < tolerance * tolerance)
mPath.pop_front();
if(mPath.empty())
{
return true;
}
}
return false;
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 = true;
}
// see header for the rationale
void PathFinder::buildSyncedPath(const ESM::Pathgrid::Point &startPoint,
const ESM::Pathgrid::Point &endPoint,
const MWWorld::CellStore* cell, const MWMechanics::PathgridGraph& pathgridGraph)
{
if (mPath.size() < 2)
void PathFinder::buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const osg::Vec3f& halfExtents,
const DetourNavigator::Flags flags)
{
// if path has one point, then it's the destination.
// don't need to worry about bad path for this case
buildPath(startPoint, endPoint, cell, pathgridGraph);
mPath.clear();
mCell = cell;
buildPathByNavigatorImpl(actor, startPoint, endPoint, halfExtents, flags, std::back_inserter(mPath));
if (mPath.empty())
buildPathByPathgridImpl(startPoint, endPoint, pathgridGraph, std::back_inserter(mPath));
mConstructed = true;
}
else
{
const ESM::Pathgrid::Point oldStart(*getPath().begin());
buildPath(startPoint, endPoint, cell, pathgridGraph);
if (mPath.size() >= 2)
void PathFinder::buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint,
const osg::Vec3f& endPoint, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags,
std::back_insert_iterator<std::deque<osg::Vec3f>> out)
{
// if 2nd waypoint of new path == 1st waypoint of old,
// delete 1st waypoint of new path.
std::list<ESM::Pathgrid::Point>::iterator iter = ++mPath.begin();
if (iter->mX == oldStart.mX
&& iter->mY == oldStart.mY
&& iter->mZ == oldStart.mZ)
try
{
mPath.pop_front();
}
const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
navigator->findPath(halfExtents, startPoint, endPoint, flags, out);
}
}
}
const MWWorld::CellStore* PathFinder::getPathCell() const
catch (const DetourNavigator::NavigatorException& exception)
{
return mCell;
DetourNavigator::log("PathFinder::buildPathByNavigator navigator exception: ", exception.what());
Log(Debug::Verbose) << "Build path by navigator exception: \"" << exception.what()
<< "\" for \"" << actor.getClass().getName(actor) << "\" (" << actor.getBase()
<< ") from " << startPoint << " to " << endPoint << " with flags ("
<< DetourNavigator::WriteFlags {flags} << ")";
}
}
}

@ -1,33 +1,57 @@
#ifndef GAME_MWMECHANICS_PATHFINDING_H
#define GAME_MWMECHANICS_PATHFINDING_H
#include <list>
#include <deque>
#include <cassert>
#include <iterator>
#include <components/detournavigator/flags.hpp>
#include <components/esm/defs.hpp>
#include <components/esm/loadpgrd.hpp>
namespace MWWorld
{
class CellStore;
class ConstPtr;
}
namespace MWMechanics
{
class PathgridGraph;
float distance(const ESM::Pathgrid::Point& point, float x, float y, float);
float distance(const ESM::Pathgrid::Point& a, const ESM::Pathgrid::Point& b);
float getZAngleToDir(const osg::Vec3f& dir);
float getXAngleToDir(const osg::Vec3f& dir);
float getZAngleToPoint(const ESM::Pathgrid::Point &origin, const ESM::Pathgrid::Point &dest);
float getXAngleToPoint(const ESM::Pathgrid::Point &origin, const ESM::Pathgrid::Point &dest);
inline float distance(const osg::Vec3f& lhs, const osg::Vec3f& rhs)
{
return (lhs - rhs).length();
}
inline float getZAngleToDir(const osg::Vec3f& dir)
{
return std::atan2(dir.x(), dir.y());
}
inline float getXAngleToDir(const osg::Vec3f& dir)
{
float dirLen = dir.length();
return (dirLen != 0) ? -std::asin(dir.z() / dirLen) : 0;
}
inline float getZAngleToPoint(const osg::Vec3f& origin, const osg::Vec3f& dest)
{
return getZAngleToDir(dest - origin);
}
inline float getXAngleToPoint(const osg::Vec3f& origin, const osg::Vec3f& dest)
{
return getXAngleToDir(dest - origin);
}
const float PATHFIND_Z_REACH = 50.0f;
//static const float sMaxSlope = 49.0f; // duplicate as in physicssystem
// distance after which actor (failed previously to shortcut) will try again
const float PATHFIND_SHORTCUT_RETRY_DIST = 300.0f;
const float DEFAULT_TOLERANCE = 32.0f;
// cast up-down ray with some offset from actor position to check for pits/obstacles on the way to target;
// magnitude of pits/obstacles is defined by PATHFIND_Z_REACH
bool checkWayIsClear(const osg::Vec3f& from, const osg::Vec3f& to, float offsetXY);
@ -35,31 +59,33 @@ namespace MWMechanics
class PathFinder
{
public:
PathFinder();
static const int PathTolerance = 32;
static float sgn(float val)
PathFinder()
: mConstructed(false)
, mCell(nullptr)
{
if(val > 0)
return 1.0;
return -1.0;
}
static int sgn(int a)
void clearPath()
{
if(a > 0)
return 1;
return -1;
mConstructed = false;
mPath.clear();
mCell = nullptr;
}
void clearPath();
void buildPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint,
void buildPathByPathgrid(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph);
bool checkPathCompleted(float x, float y, float tolerance = PathTolerance);
///< \Returns true if we are within \a tolerance units of the last path point.
void buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const osg::Vec3f& halfExtents,
const DetourNavigator::Flags flags);
/// Remove front point if exist and within tolerance
void update(const osg::Vec3f& position, const float pointTolerance, const float destinationTolerance);
bool checkPathCompleted() const
{
return mConstructed && mPath.empty();
}
/// In radians
float getZAngleToNext(float x, float y) const;
@ -68,49 +94,43 @@ namespace MWMechanics
bool isPathConstructed() const
{
return !mPath.empty();
return mConstructed && !mPath.empty();
}
int getPathSize() const
std::size_t getPathSize() const
{
return mPath.size();
}
const std::list<ESM::Pathgrid::Point>& getPath() const
const std::deque<osg::Vec3f>& getPath() const
{
return mPath;
}
const MWWorld::CellStore* getPathCell() const;
/** Synchronize new path with old one to avoid visiting 1 waypoint 2 times
@note
BuildPath() takes closest PathGrid point to NPC as first point of path.
This is undesirable if NPC has just passed a Pathgrid point, as this
makes the 2nd point of the new path == the 1st point of old path.
Which results in NPC "running in a circle" back to the just passed waypoint.
*/
void buildSyncedPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph);
const MWWorld::CellStore* getPathCell() const
{
return mCell;
}
void addPointToPath(const ESM::Pathgrid::Point &point)
void addPointToPath(const osg::Vec3f& point)
{
mConstructed = true;
mPath.push_back(point);
}
/// utility function to convert a osg::Vec3f to a Pathgrid::Point
static ESM::Pathgrid::Point MakePathgridPoint(const osg::Vec3f& v)
static ESM::Pathgrid::Point makePathgridPoint(const osg::Vec3f& v)
{
return ESM::Pathgrid::Point(static_cast<int>(v[0]), static_cast<int>(v[1]), static_cast<int>(v[2]));
}
/// utility function to convert an ESM::Position to a Pathgrid::Point
static ESM::Pathgrid::Point MakePathgridPoint(const ESM::Position& p)
static ESM::Pathgrid::Point makePathgridPoint(const ESM::Position& p)
{
return ESM::Pathgrid::Point(static_cast<int>(p.pos[0]), static_cast<int>(p.pos[1]), static_cast<int>(p.pos[2]));
}
static osg::Vec3f MakeOsgVec3(const ESM::Pathgrid::Point& p)
static osg::Vec3f makeOsgVec3(const ESM::Pathgrid::Point& p)
{
return osg::Vec3f(static_cast<float>(p.mX), static_cast<float>(p.mY), static_cast<float>(p.mZ));
}
@ -119,9 +139,9 @@ namespace MWMechanics
// 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(ESM::Pathgrid::Point point, const osg::Vec3f& pos)
static float distanceSquared(ESM::Pathgrid::Point point, const osg::Vec3f& pos)
{
return (MWMechanics::PathFinder::MakeOsgVec3(point) - pos).length2();
return (MWMechanics::PathFinder::makeOsgVec3(point) - pos).length2();
}
// Return the closest pathgrid point index from the specified position
@ -130,18 +150,18 @@ namespace MWMechanics
//
// NOTE: pos is expected to be in local coordinates, as is grid->mPoints
//
static int GetClosestPoint(const ESM::Pathgrid* grid, const osg::Vec3f& pos)
static int getClosestPoint(const ESM::Pathgrid* grid, const osg::Vec3f& pos)
{
assert(grid && !grid->mPoints.empty());
float distanceBetween = DistanceSquared(grid->mPoints[0], pos);
float distanceBetween = distanceSquared(grid->mPoints[0], pos);
int closestIndex = 0;
// TODO: if this full scan causes performance problems mapping pathgrid
// points to a quadtree may help
for(unsigned int counter = 1; counter < grid->mPoints.size(); counter++)
{
float potentialDistBetween = DistanceSquared(grid->mPoints[counter], pos);
float potentialDistBetween = distanceSquared(grid->mPoints[counter], pos);
if(potentialDistBetween < distanceBetween)
{
distanceBetween = potentialDistBetween;
@ -153,10 +173,17 @@ namespace MWMechanics
}
private:
std::list<ESM::Pathgrid::Point> mPath;
bool mConstructed;
std::deque<osg::Vec3f> mPath;
const ESM::Pathgrid *mPathgrid;
const MWWorld::CellStore* mCell;
void buildPathByPathgridImpl(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const PathgridGraph& pathgridGraph, std::back_insert_iterator<std::deque<osg::Vec3f>> out);
void buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint,
const osg::Vec3f& endPoint, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags,
std::back_insert_iterator<std::deque<osg::Vec3f>> out);
};
}

@ -257,10 +257,9 @@ namespace MWMechanics
* pathgrid points form (currently they are converted to world
* coordinates). Essentially trading speed w/ memory.
*/
std::list<ESM::Pathgrid::Point> PathgridGraph::aStarSearch(const int start,
const int goal) const
std::deque<ESM::Pathgrid::Point> PathgridGraph::aStarSearch(const int start, const int goal) const
{
std::list<ESM::Pathgrid::Point> path;
std::deque<ESM::Pathgrid::Point> path;
if(!isPointConnected(start, goal))
{
return path; // there is no path, return an empty path

@ -1,7 +1,7 @@
#ifndef GAME_MWMECHANICS_PATHGRID_H
#define GAME_MWMECHANICS_PATHGRID_H
#include <list>
#include <deque>
#include <components/esm/loadpgrd.hpp>
@ -38,8 +38,8 @@ namespace MWMechanics
// cells) coordinates
//
// NOTE: if start equals end an empty path is returned
std::list<ESM::Pathgrid::Point> aStarSearch(const int start,
const int end) const;
std::deque<ESM::Pathgrid::Point> aStarSearch(const int start, const int end) const;
private:
const ESM::Cell *mCell;

@ -324,6 +324,34 @@ namespace MWMechanics
return true;
}
class GetAbsorptionProbability : public MWMechanics::EffectSourceVisitor
{
public:
float mProbability;
GetAbsorptionProbability(const MWWorld::Ptr& actor)
: mProbability(0.f){}
virtual void visit (MWMechanics::EffectKey key,
const std::string& sourceName, const std::string& sourceId, int casterActorId,
float magnitude, float remainingTime = -1, float totalTime = -1)
{
if (key.mId == ESM::MagicEffect::SpellAbsorption)
{
if (mProbability == 0.f)
mProbability = magnitude / 100;
else
{
// If there are different sources of SpellAbsorption effect, multiply failing probability for all effects.
// Real absorption probability will be the (1 - total fail chance) in this case.
float failProbability = 1.f - mProbability;
failProbability *= 1.f - magnitude / 100;
mProbability = 1.f - failProbability;
}
}
}
};
CastSpell::CastSpell(const MWWorld::Ptr &caster, const MWWorld::Ptr &target, const bool fromProjectile, const bool manualSpell)
: mCaster(caster)
, mTarget(target)
@ -444,12 +472,20 @@ namespace MWMechanics
MWBase::Environment::get().getWindowManager()->setEnemy(target);
// Try absorbing if it's a spell
// NOTE: Vanilla does this once per spell absorption effect source instead of adding the % from all sources together, not sure
// if that is worth replicating.
// Unlike Reflect, this is done once per spell absorption effect source
bool absorbed = false;
if (spell && caster != target && target.getClass().isActor())
{
float absorb = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::SpellAbsorption).getMagnitude();
CreatureStats& stats = target.getClass().getCreatureStats(target);
if (stats.getMagicEffects().get(ESM::MagicEffect::SpellAbsorption).getMagnitude() > 0.f)
{
GetAbsorptionProbability check(target);
stats.getActiveSpells().visitEffectSources(check);
stats.getSpells().visitEffectSources(check);
if (target.getClass().hasInventoryStore(target))
target.getClass().getInventoryStore(target).visitEffectSources(check);
int absorb = check.mProbability * 100;
absorbed = (Misc::Rng::roll0to99() < absorb);
if (absorbed)
{
@ -457,9 +493,10 @@ namespace MWMechanics
MWBase::Environment::get().getWorld()->getAnimation(target)->addEffect(
"meshes\\" + absorbStatic->mModel, ESM::MagicEffect::SpellAbsorption, false, "");
// Magicka is increased by cost of spell
DynamicStat<float> magicka = target.getClass().getCreatureStats(target).getMagicka();
DynamicStat<float> magicka = stats.getMagicka();
magicka.setCurrent(magicka.getCurrent() + spell->mData.mCost);
target.getClass().getCreatureStats(target).setMagicka(magicka);
stats.setMagicka(magicka);
}
}
}

@ -58,8 +58,16 @@ namespace MWMechanics
}
const float chop = (weapon->mData.mChop[0] + weapon->mData.mChop[1]) / 2.f;
if (weapon->mData.mType >= ESM::Weapon::MarksmanBow)
// We need to account for the fact that thrown weapons have 2x real damage applied to the target
// as they're both the weapon and the ammo of the hit
if (weapon->mData.mType == ESM::Weapon::MarksmanThrown)
{
rating = chop * 2;
}
else if (weapon->mData.mType >= ESM::Weapon::MarksmanBow)
{
rating = chop;
}
else
{
const float slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1]) / 2.f;

@ -6,6 +6,7 @@
#include <components/sceneutil/positionattitudetransform.hpp>
#include <components/resource/bulletshape.hpp>
#include <components/debug/debuglog.hpp>
#include "../mwworld/class.hpp"
@ -28,6 +29,31 @@ Actor::Actor(const MWWorld::Ptr& ptr, osg::ref_ptr<const Resource::BulletShape>
mHalfExtents = shape->mCollisionBoxHalfExtents;
mMeshTranslation = shape->mCollisionBoxTranslate;
// We can not create actor without collisions - he will fall through the ground.
// In this case we should autogenerate collision box based on mesh shape
// (NPCs have bodyparts and use a different approach)
if (!ptr.getClass().isNpc() && mHalfExtents.length2() == 0.f)
{
const Resource::BulletShape* collisionShape = shape.get();
if (collisionShape && collisionShape->mCollisionShape)
{
btTransform transform;
transform.setIdentity();
btVector3 min;
btVector3 max;
collisionShape->mCollisionShape->getAabb(transform, min, max);
mHalfExtents.x() = (max[0] - min[0])/2.f;
mHalfExtents.y() = (max[1] - min[1])/2.f;
mHalfExtents.z() = (max[2] - min[2])/2.f;
mMeshTranslation = osg::Vec3f(0.f, 0.f, mHalfExtents.z());
}
if (mHalfExtents.length2() == 0.f)
Log(Debug::Error) << "Error: Failed to calculate bounding box for actor \"" << ptr.getCellRef().getRefId() << "\".";
}
// Use capsule shape only if base is square (nonuniform scaling apparently doesn't work on it)
if (std::abs(mHalfExtents.x()-mHalfExtents.y())<mHalfExtents.x()*0.05 && mHalfExtents.z() >= mHalfExtents.x())
{

@ -3,7 +3,7 @@
#include <memory>
#include "../mwworld/ptr.hpp"
#include "ptrholder.hpp"
#include <osg/Vec3f>
#include <osg/Quat>
@ -22,30 +22,6 @@ namespace Resource
namespace MWPhysics
{
class PtrHolder
{
public:
virtual ~PtrHolder() {}
void updatePtr(const MWWorld::Ptr& updated)
{
mPtr = updated;
}
MWWorld::Ptr getPtr()
{
return mPtr;
}
MWWorld::ConstPtr getPtr() const
{
return mPtr;
}
protected:
MWWorld::Ptr mPtr;
};
class Actor : public PtrHolder
{
public:

@ -0,0 +1,54 @@
#include "heightfield.hpp"
#include <osg/Object>
#include <BulletCollision/CollisionShapes/btHeightfieldTerrainShape.h>
#include <BulletCollision/CollisionDispatch/btCollisionObject.h>
#include <LinearMath/btTransform.h>
namespace MWPhysics
{
HeightField::HeightField(const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject)
{
mShape = new btHeightfieldTerrainShape(
sqrtVerts, sqrtVerts, heights, 1,
minH, maxH, 2,
PHY_FLOAT, false
);
mShape->setUseDiamondSubdivision(true);
mShape->setLocalScaling(btVector3(triSize, triSize, 1));
btTransform transform(btQuaternion::getIdentity(),
btVector3((x+0.5f) * triSize * (sqrtVerts-1),
(y+0.5f) * triSize * (sqrtVerts-1),
(maxH+minH)*0.5f));
mCollisionObject = new btCollisionObject;
mCollisionObject->setCollisionShape(mShape);
mCollisionObject->setWorldTransform(transform);
mHoldObject = holdObject;
}
HeightField::~HeightField()
{
delete mCollisionObject;
delete mShape;
}
btCollisionObject* HeightField::getCollisionObject()
{
return mCollisionObject;
}
const btCollisionObject* HeightField::getCollisionObject() const
{
return mCollisionObject;
}
const btHeightfieldTerrainShape* HeightField::getShape() const
{
return mShape;
}
}

@ -0,0 +1,36 @@
#ifndef OPENMW_MWPHYSICS_HEIGHTFIELD_H
#define OPENMW_MWPHYSICS_HEIGHTFIELD_H
#include <osg/ref_ptr>
class btCollisionObject;
class btHeightfieldTerrainShape;
namespace osg
{
class Object;
}
namespace MWPhysics
{
class HeightField
{
public:
HeightField(const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject);
~HeightField();
btCollisionObject* getCollisionObject();
const btCollisionObject* getCollisionObject() const;
const btHeightfieldTerrainShape* getShape() const;
private:
btHeightfieldTerrainShape* mShape;
btCollisionObject* mCollisionObject;
osg::ref_ptr<const osg::Object> mHoldObject;
void operator=(const HeightField&);
HeightField(const HeightField&);
};
}
#endif

@ -0,0 +1,130 @@
#include "object.hpp"
#include "convert.hpp"
#include <components/debug/debuglog.hpp>
#include <components/nifosg/particle.hpp>
#include <components/resource/bulletshape.hpp>
#include <components/sceneutil/positionattitudetransform.hpp>
#include <osg/Object>
#include <BulletCollision/CollisionShapes/btCompoundShape.h>
#include <BulletCollision/CollisionDispatch/btCollisionObject.h>
#include <BulletCollision/CollisionDispatch/btCollisionWorld.h>
#include <LinearMath/btTransform.h>
namespace MWPhysics
{
Object::Object(const MWWorld::Ptr& ptr, osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance)
: mShapeInstance(shapeInstance)
, mSolid(true)
{
mPtr = ptr;
mCollisionObject.reset(new btCollisionObject);
mCollisionObject->setCollisionShape(shapeInstance->getCollisionShape());
mCollisionObject->setUserPointer(static_cast<PtrHolder*>(this));
setScale(ptr.getCellRef().getScale());
setRotation(toBullet(ptr.getRefData().getBaseNode()->getAttitude()));
const float* pos = ptr.getRefData().getPosition().pos;
setOrigin(btVector3(pos[0], pos[1], pos[2]));
}
const Resource::BulletShapeInstance* Object::getShapeInstance() const
{
return mShapeInstance.get();
}
void Object::setScale(float scale)
{
mShapeInstance->setLocalScaling(btVector3(scale, scale, scale));
}
void Object::setRotation(const btQuaternion& quat)
{
mCollisionObject->getWorldTransform().setRotation(quat);
}
void Object::setOrigin(const btVector3& vec)
{
mCollisionObject->getWorldTransform().setOrigin(vec);
}
btCollisionObject* Object::getCollisionObject()
{
return mCollisionObject.get();
}
const btCollisionObject* Object::getCollisionObject() const
{
return mCollisionObject.get();
}
bool Object::isSolid() const
{
return mSolid;
}
void Object::setSolid(bool solid)
{
mSolid = solid;
}
bool Object::isAnimated() const
{
return !mShapeInstance->mAnimatedShapes.empty();
}
void Object::animateCollisionShapes(btCollisionWorld* collisionWorld)
{
if (mShapeInstance->mAnimatedShapes.empty())
return;
assert (mShapeInstance->getCollisionShape()->isCompound());
btCompoundShape* compound = static_cast<btCompoundShape*>(mShapeInstance->getCollisionShape());
for (std::map<int, int>::const_iterator it = mShapeInstance->mAnimatedShapes.begin(); it != mShapeInstance->mAnimatedShapes.end(); ++it)
{
int recIndex = it->first;
int shapeIndex = it->second;
std::map<int, osg::NodePath>::iterator nodePathFound = mRecIndexToNodePath.find(recIndex);
if (nodePathFound == mRecIndexToNodePath.end())
{
NifOsg::FindGroupByRecIndex visitor(recIndex);
mPtr.getRefData().getBaseNode()->accept(visitor);
if (!visitor.mFound)
{
Log(Debug::Warning) << "Warning: animateCollisionShapes can't find node " << recIndex << " for " << mPtr.getCellRef().getRefId();
// Remove nonexistent nodes from animated shapes map and early out
mShapeInstance->mAnimatedShapes.erase(recIndex);
return;
}
osg::NodePath nodePath = visitor.mFoundPath;
nodePath.erase(nodePath.begin());
nodePathFound = mRecIndexToNodePath.insert(std::make_pair(recIndex, nodePath)).first;
}
osg::NodePath& nodePath = nodePathFound->second;
osg::Matrixf matrix = osg::computeLocalToWorld(nodePath);
matrix.orthoNormalize(matrix);
btTransform transform;
transform.setOrigin(toBullet(matrix.getTrans()) * compound->getLocalScaling());
for (int i=0; i<3; ++i)
for (int j=0; j<3; ++j)
transform.getBasis()[i][j] = matrix(j,i); // NB column/row major difference
// Note: we can not apply scaling here for now since we treat scaled shapes
// as new shapes (btScaledBvhTriangleMeshShape) with 1.0 scale for now
if (!(transform == compound->getChildTransform(shapeIndex)))
compound->updateChildTransform(shapeIndex, transform);
}
collisionWorld->updateSingleAabb(mCollisionObject.get());
}
}

@ -0,0 +1,48 @@
#ifndef OPENMW_MWPHYSICS_OBJECT_H
#define OPENMW_MWPHYSICS_OBJECT_H
#include "ptrholder.hpp"
#include <osg/Node>
#include <map>
#include <memory>
namespace Resource
{
class BulletShapeInstance;
}
class btCollisionObject;
class btCollisionWorld;
class btQuaternion;
class btVector3;
namespace MWPhysics
{
class Object : public PtrHolder
{
public:
Object(const MWWorld::Ptr& ptr, osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance);
const Resource::BulletShapeInstance* getShapeInstance() const;
void setScale(float scale);
void setRotation(const btQuaternion& quat);
void setOrigin(const btVector3& vec);
btCollisionObject* getCollisionObject();
const btCollisionObject* getCollisionObject() const;
/// Return solid flag. Not used by the object itself, true by default.
bool isSolid() const;
void setSolid(bool solid);
bool isAnimated() const;
void animateCollisionShapes(btCollisionWorld* collisionWorld);
private:
std::unique_ptr<btCollisionObject> mCollisionObject;
osg::ref_ptr<Resource::BulletShapeInstance> mShapeInstance;
std::map<int, osg::NodePath> mRecIndexToNodePath;
bool mSolid;
};
}
#endif

@ -1,6 +1,11 @@
#include "physicssystem.hpp"
#include "physicssystem.hpp"
#include <stdexcept>
#include <unordered_map>
#include <fstream>
#include <array>
#include <boost/optional.hpp>
#include <osg/Group>
@ -16,6 +21,12 @@
#include <LinearMath/btQuickprof.h>
#include <DetourCommon.h>
#include <DetourNavMesh.h>
#include <DetourNavMeshBuilder.h>
#include <DetourNavMeshQuery.h>
#include <Recast.h>
#include <components/nifbullet/bulletnifloader.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/resource/bulletshapemanager.hpp>
@ -48,12 +59,12 @@
#include "actor.hpp"
#include "convert.hpp"
#include "trace.h"
#include "object.hpp"
#include "heightfield.hpp"
namespace MWPhysics
{
static const float sMaxSlope = 49.0f;
static const float sStepSizeUp = 34.0f;
static const float sStepSizeDown = 62.0f;
static const float sMinStep = 10.f;
static const float sGroundOffset = 1.0f;
@ -507,175 +518,6 @@ namespace MWPhysics
// ---------------------------------------------------------------
class HeightField
{
public:
HeightField(const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject)
{
mShape = new btHeightfieldTerrainShape(
sqrtVerts, sqrtVerts, heights, 1,
minH, maxH, 2,
PHY_FLOAT, false
);
mShape->setUseDiamondSubdivision(true);
mShape->setLocalScaling(btVector3(triSize, triSize, 1));
btTransform transform(btQuaternion::getIdentity(),
btVector3((x+0.5f) * triSize * (sqrtVerts-1),
(y+0.5f) * triSize * (sqrtVerts-1),
(maxH+minH)*0.5f));
mCollisionObject = new btCollisionObject;
mCollisionObject->setCollisionShape(mShape);
mCollisionObject->setWorldTransform(transform);
mHoldObject = holdObject;
}
~HeightField()
{
delete mCollisionObject;
delete mShape;
}
btCollisionObject* getCollisionObject()
{
return mCollisionObject;
}
private:
btHeightfieldTerrainShape* mShape;
btCollisionObject* mCollisionObject;
osg::ref_ptr<const osg::Object> mHoldObject;
void operator=(const HeightField&);
HeightField(const HeightField&);
};
// --------------------------------------------------------------
class Object : public PtrHolder
{
public:
Object(const MWWorld::Ptr& ptr, osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance)
: mShapeInstance(shapeInstance)
, mSolid(true)
{
mPtr = ptr;
mCollisionObject.reset(new btCollisionObject);
mCollisionObject->setCollisionShape(shapeInstance->getCollisionShape());
mCollisionObject->setUserPointer(static_cast<PtrHolder*>(this));
setScale(ptr.getCellRef().getScale());
setRotation(toBullet(ptr.getRefData().getBaseNode()->getAttitude()));
const float* pos = ptr.getRefData().getPosition().pos;
setOrigin(btVector3(pos[0], pos[1], pos[2]));
}
const Resource::BulletShapeInstance* getShapeInstance() const
{
return mShapeInstance.get();
}
void setScale(float scale)
{
mShapeInstance->getCollisionShape()->setLocalScaling(btVector3(scale,scale,scale));
}
void setRotation(const btQuaternion& quat)
{
mCollisionObject->getWorldTransform().setRotation(quat);
}
void setOrigin(const btVector3& vec)
{
mCollisionObject->getWorldTransform().setOrigin(vec);
}
btCollisionObject* getCollisionObject()
{
return mCollisionObject.get();
}
const btCollisionObject* getCollisionObject() const
{
return mCollisionObject.get();
}
/// Return solid flag. Not used by the object itself, true by default.
bool isSolid() const
{
return mSolid;
}
void setSolid(bool solid)
{
mSolid = solid;
}
bool isAnimated() const
{
return !mShapeInstance->mAnimatedShapes.empty();
}
void animateCollisionShapes(btCollisionWorld* collisionWorld)
{
if (mShapeInstance->mAnimatedShapes.empty())
return;
assert (mShapeInstance->getCollisionShape()->isCompound());
btCompoundShape* compound = static_cast<btCompoundShape*>(mShapeInstance->getCollisionShape());
for (std::map<int, int>::const_iterator it = mShapeInstance->mAnimatedShapes.begin(); it != mShapeInstance->mAnimatedShapes.end(); ++it)
{
int recIndex = it->first;
int shapeIndex = it->second;
std::map<int, osg::NodePath>::iterator nodePathFound = mRecIndexToNodePath.find(recIndex);
if (nodePathFound == mRecIndexToNodePath.end())
{
NifOsg::FindGroupByRecIndex visitor(recIndex);
mPtr.getRefData().getBaseNode()->accept(visitor);
if (!visitor.mFound)
{
Log(Debug::Warning) << "Warning: animateCollisionShapes can't find node " << recIndex << " for " << mPtr.getCellRef().getRefId();
// Remove nonexistent nodes from animated shapes map and early out
mShapeInstance->mAnimatedShapes.erase(recIndex);
return;
}
osg::NodePath nodePath = visitor.mFoundPath;
nodePath.erase(nodePath.begin());
nodePathFound = mRecIndexToNodePath.insert(std::make_pair(recIndex, nodePath)).first;
}
osg::NodePath& nodePath = nodePathFound->second;
osg::Matrixf matrix = osg::computeLocalToWorld(nodePath);
matrix.orthoNormalize(matrix);
btTransform transform;
transform.setOrigin(toBullet(matrix.getTrans()) * compound->getLocalScaling());
for (int i=0; i<3; ++i)
for (int j=0; j<3; ++j)
transform.getBasis()[i][j] = matrix(j,i); // NB column/row major difference
// Note: we can not apply scaling here for now since we treat scaled shapes
// as new shapes (btScaledBvhTriangleMeshShape) with 1.0 scale for now
if (!(transform == compound->getChildTransform(shapeIndex)))
compound->updateChildTransform(shapeIndex, transform);
}
collisionWorld->updateSingleAabb(mCollisionObject.get());
}
private:
std::unique_ptr<btCollisionObject> mCollisionObject;
osg::ref_ptr<Resource::BulletShapeInstance> mShapeInstance;
std::map<int, osg::NodePath> mRecIndexToNodePath;
bool mSolid;
};
// ---------------------------------------------------------------
PhysicsSystem::PhysicsSystem(Resource::ResourceSystem* resourceSystem, osg::ref_ptr<osg::Group> parentNode)
: mShapeManager(new Resource::BulletShapeManager(resourceSystem->getVFS(), resourceSystem->getSceneManager(), resourceSystem->getNifFileManager()))
@ -1171,6 +1013,14 @@ namespace MWPhysics
}
}
const HeightField* PhysicsSystem::getHeightField(int x, int y) const
{
const auto heightField = mHeightFields.find(std::make_pair(x, y));
if (heightField == mHeightFields.end())
return nullptr;
return heightField->second;
}
void PhysicsSystem::addObject (const MWWorld::Ptr& ptr, const std::string& mesh, int collisionType)
{
osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance = mShapeManager->getInstance(mesh);
@ -1366,6 +1216,33 @@ namespace MWPhysics
return false;
}
void PhysicsSystem::setActorCollisionMode(const MWWorld::Ptr& ptr, bool enabled)
{
ActorMap::iterator found = mActors.find(ptr);
if (found != mActors.end())
{
bool cmode = found->second->getCollisionMode();
if (cmode == enabled)
return;
cmode = enabled;
found->second->enableCollisionMode(cmode);
found->second->enableCollisionBody(cmode);
}
}
bool PhysicsSystem::isActorCollisionEnabled(const MWWorld::Ptr& ptr)
{
ActorMap::iterator found = mActors.find(ptr);
if (found != mActors.end())
{
bool cmode = found->second->getCollisionMode();
return cmode;
}
return false;
}
void PhysicsSystem::queueObjectMovement(const MWWorld::Ptr &ptr, const osg::Vec3f &movement)
{
PtrVelocityList::iterator iter = mMovementQueue.begin();

@ -4,6 +4,7 @@
#include <memory>
#include <map>
#include <set>
#include <algorithm>
#include <osg/Quat>
#include <osg/ref_ptr>
@ -49,6 +50,9 @@ namespace MWPhysics
class Object;
class Actor;
static const float sMaxSlope = 49.0f;
static const float sStepSizeUp = 34.0f;
class PhysicsSystem
{
public:
@ -85,7 +89,11 @@ namespace MWPhysics
void removeHeightField (int x, int y);
const HeightField* getHeightField(int x, int y) const;
bool toggleCollisionMode();
bool isActorCollisionEnabled(const MWWorld::Ptr& ptr);
void setActorCollisionMode(const MWWorld::Ptr& ptr, bool enabled);
void stepSimulation(float dt);
void debugDraw();
@ -170,6 +178,12 @@ namespace MWPhysics
bool isOnSolidGround (const MWWorld::Ptr& actor) const;
template <class Function>
void forEachAnimatedObject(Function&& function) const
{
std::for_each(mAnimatedObjects.begin(), mAnimatedObjects.end(), function);
}
private:
void updateWater();

@ -0,0 +1,33 @@
#ifndef OPENMW_MWPHYSICS_PTRHOLDER_H
#define OPENMW_MWPHYSICS_PTRHOLDER_H
#include "../mwworld/ptr.hpp"
namespace MWPhysics
{
class PtrHolder
{
public:
virtual ~PtrHolder() {}
void updatePtr(const MWWorld::Ptr& updated)
{
mPtr = updated;
}
MWWorld::Ptr getPtr()
{
return mPtr;
}
MWWorld::ConstPtr getPtr() const
{
return mPtr;
}
protected:
MWWorld::Ptr mPtr;
};
}
#endif

@ -0,0 +1,99 @@
#include "actorspaths.hpp"
#include "vismask.hpp"
#include <components/sceneutil/agentpath.hpp>
#include <osg/PositionAttitudeTransform>
namespace MWRender
{
ActorsPaths::ActorsPaths(const osg::ref_ptr<osg::Group>& root, bool enabled)
: mRootNode(root)
, mEnabled(enabled)
{
}
ActorsPaths::~ActorsPaths()
{
if (mEnabled)
disable();
}
bool ActorsPaths::toggle()
{
if (mEnabled)
disable();
else
enable();
return mEnabled;
}
void ActorsPaths::update(const MWWorld::ConstPtr& actor, const std::deque<osg::Vec3f>& path,
const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end,
const DetourNavigator::Settings& settings)
{
if (!mEnabled)
return;
const auto group = mGroups.find(actor);
if (group != mGroups.end())
mRootNode->removeChild(group->second);
const auto newGroup = SceneUtil::createAgentPathGroup(path, halfExtents, start, end, settings);
if (newGroup)
{
newGroup->setNodeMask(Mask_Debug);
mRootNode->addChild(newGroup);
mGroups[actor] = newGroup;
}
}
void ActorsPaths::remove(const MWWorld::ConstPtr& actor)
{
const auto group = mGroups.find(actor);
if (group != mGroups.end())
{
mRootNode->removeChild(group->second);
mGroups.erase(group);
}
}
void ActorsPaths::removeCell(const MWWorld::CellStore* const store)
{
for (auto it = mGroups.begin(); it != mGroups.end(); )
{
if (it->first.getCell() == store)
{
mRootNode->removeChild(it->second);
it = mGroups.erase(it);
}
else
++it;
}
}
void ActorsPaths::updatePtr(const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& updated)
{
const auto it = mGroups.find(old);
if (it == mGroups.end())
return;
auto group = std::move(it->second);
mGroups.erase(it);
mGroups.insert(std::make_pair(updated, std::move(group)));
}
void ActorsPaths::enable()
{
std::for_each(mGroups.begin(), mGroups.end(),
[&] (const Groups::value_type& v) { mRootNode->addChild(v.second); });
mEnabled = true;
}
void ActorsPaths::disable()
{
std::for_each(mGroups.begin(), mGroups.end(),
[&] (const Groups::value_type& v) { mRootNode->removeChild(v.second); });
mEnabled = false;
}
}

@ -0,0 +1,51 @@
#ifndef OPENMW_MWRENDER_AGENTSPATHS_H
#define OPENMW_MWRENDER_AGENTSPATHS_H
#include <apps/openmw/mwworld/ptr.hpp>
#include <components/detournavigator/navigator.hpp>
#include <osg/ref_ptr>
#include <unordered_map>
#include <deque>
namespace osg
{
class Group;
}
namespace MWRender
{
class ActorsPaths
{
public:
ActorsPaths(const osg::ref_ptr<osg::Group>& root, bool enabled);
~ActorsPaths();
bool toggle();
void update(const MWWorld::ConstPtr& actor, const std::deque<osg::Vec3f>& path,
const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end,
const DetourNavigator::Settings& settings);
void remove(const MWWorld::ConstPtr& actor);
void removeCell(const MWWorld::CellStore* const store);
void updatePtr(const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& updated);
void enable();
void disable();
private:
using Groups = std::map<MWWorld::ConstPtr, osg::ref_ptr<osg::Group>>;
osg::ref_ptr<osg::Group> mRootNode;
Groups mGroups;
bool mEnabled;
};
}
#endif

@ -15,13 +15,13 @@
#include <components/debug/debuglog.hpp>
#include <components/nifosg/nifloader.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/resource/scenemanager.hpp>
#include <components/resource/keyframemanager.hpp>
#include <components/resource/imagemanager.hpp>
#include <components/misc/constants.hpp>
#include <components/nifosg/nifloader.hpp> // KeyframeHolder
#include <components/nifosg/controller.hpp>
@ -190,12 +190,6 @@ namespace
{
}
RemoveFinishedCallbackVisitor(int effectId)
: RemoveVisitor()
, mHasMagicEffects(false)
{
}
virtual void apply(osg::Node &node)
{
traverse(node);
@ -228,9 +222,6 @@ namespace
virtual void apply(osg::Geometry&)
{
}
private:
int mEffectId;
};
class RemoveCallbackVisitor : public RemoveVisitor
@ -1747,6 +1738,15 @@ namespace MWRender
mAlpha = alpha;
if (alpha != 1.f)
{
// If we have an existing material for alpha transparency, just override alpha level
osg::StateSet* stateset = mObjectRoot->getOrCreateStateSet();
osg::Material* material = static_cast<osg::Material*>(stateset->getAttribute(osg::StateAttribute::MATERIAL));
if (material)
{
material->setAlpha(osg::Material::FRONT_AND_BACK, alpha);
}
else
{
osg::StateSet* stateset (new osg::StateSet);
@ -1754,7 +1754,7 @@ namespace MWRender
stateset->setAttributeAndModes(blendfunc, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE);
// FIXME: overriding diffuse/ambient/emissive colors
osg::Material* material (new osg::Material);
material = new osg::Material;
material->setColorMode(osg::Material::OFF);
material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,alpha));
material->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1));
@ -1764,6 +1764,7 @@ namespace MWRender
mResourceSystem->getSceneManager()->recreateShaders(mObjectRoot);
}
}
else
{
mObjectRoot->setStateSet(nullptr);
@ -1798,9 +1799,12 @@ namespace MWRender
}
else
{
effect += 3;
float radius = effect * 66.f;
float linearAttenuation = 0.5f / effect;
// TODO: use global attenuation settings
// 1 pt of Light magnitude corresponds to 1 foot of radius
float radius = effect * std::ceil(Constants::UnitsPerFoot);
const float linearValue = 3.f; // Currently hardcoded: unmodified Morrowind attenuation settings
float linearAttenuation = linearValue / radius;
if (!mGlowLight || linearAttenuation != mGlowLight->getLight(0)->getLinearAttenuation())
{
@ -1822,7 +1826,8 @@ namespace MWRender
mGlowLight->setLight(light);
}
mGlowLight->setRadius(radius);
// Make the obvious cut-off a bit less obvious
mGlowLight->setRadius(radius * 3);
}
}

@ -0,0 +1,71 @@
#include "navmesh.hpp"
#include "vismask.hpp"
#include <components/sceneutil/navmesh.hpp>
#include <osg/PositionAttitudeTransform>
namespace MWRender
{
NavMesh::NavMesh(const osg::ref_ptr<osg::Group>& root, bool enabled)
: mRootNode(root)
, mEnabled(enabled)
, mGeneration(0)
, mRevision(0)
{
}
NavMesh::~NavMesh()
{
if (mEnabled)
disable();
}
bool NavMesh::toggle()
{
if (mEnabled)
disable();
else
enable();
return mEnabled;
}
void NavMesh::update(const dtNavMesh& navMesh, const std::size_t id,
const std::size_t generation, const std::size_t revision, const DetourNavigator::Settings& settings)
{
if (!mEnabled || (mId == id && mGeneration >= generation && mRevision >= revision))
return;
mId = id;
mGeneration = generation;
mRevision = revision;
if (mGroup)
mRootNode->removeChild(mGroup);
mGroup = SceneUtil::createNavMeshGroup(navMesh, settings);
if (mGroup)
{
mGroup->setNodeMask(Mask_Debug);
mRootNode->addChild(mGroup);
}
}
void NavMesh::reset()
{
if (mGroup)
mRootNode->removeChild(mGroup);
}
void NavMesh::enable()
{
if (mGroup)
mRootNode->addChild(mGroup);
mEnabled = true;
}
void NavMesh::disable()
{
reset();
mEnabled = false;
}
}

@ -0,0 +1,43 @@
#ifndef OPENMW_MWRENDER_NAVMESH_H
#define OPENMW_MWRENDER_NAVMESH_H
#include <components/detournavigator/navigator.hpp>
#include <osg/ref_ptr>
namespace osg
{
class Group;
class Geometry;
}
namespace MWRender
{
class NavMesh
{
public:
NavMesh(const osg::ref_ptr<osg::Group>& root, bool enabled);
~NavMesh();
bool toggle();
void update(const dtNavMesh& navMesh, const std::size_t number, const std::size_t generation,
const std::size_t revision, const DetourNavigator::Settings& settings);
void reset();
void enable();
void disable();
private:
osg::ref_ptr<osg::Group> mRootNode;
bool mEnabled;
std::size_t mId = std::numeric_limits<std::size_t>::max();
std::size_t mGeneration;
std::size_t mRevision;
osg::ref_ptr<osg::Group> mGroup;
};
}
#endif

@ -47,6 +47,8 @@
#include <components/esm/loadcell.hpp>
#include <components/fallback/fallback.hpp>
#include <components/detournavigator/navigator.hpp>
#include <boost/algorithm/string.hpp>
#include "../mwworld/cellstore.hpp"
@ -64,6 +66,8 @@
#include "water.hpp"
#include "terrainstorage.hpp"
#include "util.hpp"
#include "navmesh.hpp"
#include "actorspaths.hpp"
namespace
{
@ -190,13 +194,16 @@ namespace MWRender
Resource::ResourceSystem* mResourceSystem;
};
RenderingManager::RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr<osg::Group> rootNode, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue,
const Fallback::Map* fallback, const std::string& resourcePath)
RenderingManager::RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr<osg::Group> rootNode,
Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue,
const Fallback::Map* fallback, const std::string& resourcePath,
DetourNavigator::Navigator& navigator)
: mViewer(viewer)
, mRootNode(rootNode)
, mResourceSystem(resourceSystem)
, mWorkQueue(workQueue)
, mUnrefQueue(new SceneUtil::UnrefQueue)
, mNavigator(navigator)
, mLandFogStart(0.f)
, mLandFogEnd(std::numeric_limits<float>::max())
, mUnderwaterFogStart(0.f)
@ -250,6 +257,8 @@ namespace MWRender
// It is unnecessary to stop/start the viewer as no frames are being rendered yet.
mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(globalDefines);
mNavMesh.reset(new NavMesh(mRootNode, Settings::Manager::getBool("enable nav mesh render", "Navigator")));
mActorsPaths.reset(new ActorsPaths(mRootNode, Settings::Manager::getBool("enable agents paths render", "Navigator")));
mPathgrid.reset(new Pathgrid(mRootNode));
mObjects.reset(new Objects(mResourceSystem, sceneRoot, mUnrefQueue.get()));
@ -487,6 +496,7 @@ namespace MWRender
void RenderingManager::removeCell(const MWWorld::CellStore *store)
{
mPathgrid->removeCell(store);
mActorsPaths->removeCell(store);
mObjects->removeCell(store);
if (store->getCell()->isExterior())
@ -542,6 +552,14 @@ namespace MWRender
mViewer->getCamera()->setCullMask(mask);
return enabled;
}
else if (mode == Render_NavMesh)
{
return mNavMesh->toggle();
}
else if (mode == Render_ActorsPaths)
{
return mActorsPaths->toggle();
}
return false;
}
@ -607,6 +625,28 @@ namespace MWRender
mWater->update(dt);
}
const auto navMeshes = mNavigator.getNavMeshes();
auto it = navMeshes.begin();
for (std::size_t i = 0; it != navMeshes.end() && i < mNavMeshNumber; ++i)
++it;
if (it == navMeshes.end())
{
mNavMesh->reset();
}
else
{
try
{
const auto locked = it->second.lockConst();
mNavMesh->update(locked->getValue(), mNavMeshNumber, locked->getGeneration(),
locked->getNavMeshRevision(), mNavigator.getSettings());
}
catch (const std::exception& e)
{
Log(Debug::Error) << "NavMesh render update exception: " << e.what();
}
}
mCamera->update(dt, paused);
osg::Vec3f focal, cameraPos;
@ -668,6 +708,7 @@ namespace MWRender
void RenderingManager::removeObject(const MWWorld::Ptr &ptr)
{
mActorsPaths->remove(ptr);
mObjects->removeObject(ptr);
mWater->removeEmitter(ptr);
}
@ -1051,6 +1092,7 @@ namespace MWRender
void RenderingManager::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &updated)
{
mObjects->updatePtr(old, updated);
mActorsPaths->updatePtr(old, updated);
}
void RenderingManager::spawnEffect(const std::string &model, const std::string &texture, const osg::Vec3f &worldPosition, float scale, bool isMagicVFX)
@ -1371,5 +1413,19 @@ namespace MWRender
return mTerrainStorage->getLandManager();
}
void RenderingManager::updateActorPath(const MWWorld::ConstPtr& actor, const std::deque<osg::Vec3f>& path,
const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const
{
mActorsPaths->update(actor, path, halfExtents, start, end, mNavigator.getSettings());
}
void RenderingManager::removeActorPath(const MWWorld::ConstPtr& actor) const
{
mActorsPaths->remove(actor);
}
void RenderingManager::setNavMeshNumber(const std::size_t value)
{
mNavMeshNumber = value;
}
}

@ -12,6 +12,8 @@
#include "renderinginterface.hpp"
#include "rendermode.hpp"
#include <deque>
namespace osg
{
class Group;
@ -56,6 +58,12 @@ namespace SceneUtil
class UnrefQueue;
}
namespace DetourNavigator
{
class Navigator;
struct Settings;
}
namespace MWRender
{
@ -69,12 +77,16 @@ namespace MWRender
class Water;
class TerrainStorage;
class LandManager;
class NavMesh;
class ActorsPaths;
class RenderingManager : public MWRender::RenderingInterface
{
public:
RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr<osg::Group> rootNode, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue,
const Fallback::Map* fallback, const std::string& resourcePath);
RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr<osg::Group> rootNode,
Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue,
const Fallback::Map* fallback, const std::string& resourcePath,
DetourNavigator::Navigator& navigator);
~RenderingManager();
MWRender::Objects& getObjects();
@ -212,6 +224,13 @@ namespace MWRender
bool toggleBorders();
void updateActorPath(const MWWorld::ConstPtr& actor, const std::deque<osg::Vec3f>& path,
const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const;
void removeActorPath(const MWWorld::ConstPtr& actor) const;
void setNavMeshNumber(const std::size_t value);
private:
void updateProjectionMatrix();
void updateTextureFiltering();
@ -236,6 +255,10 @@ namespace MWRender
osg::ref_ptr<osg::Light> mSunLight;
DetourNavigator::Navigator& mNavigator;
std::unique_ptr<NavMesh> mNavMesh;
std::size_t mNavMeshNumber = 0;
std::unique_ptr<ActorsPaths> mActorsPaths;
std::unique_ptr<Pathgrid> mPathgrid;
std::unique_ptr<Objects> mObjects;
std::unique_ptr<Water> mWater;

@ -10,7 +10,9 @@ namespace MWRender
Render_Wireframe,
Render_Pathgrid,
Render_Water,
Render_Scene
Render_Scene,
Render_NavMesh,
Render_ActorsPaths,
};
}

@ -455,5 +455,8 @@ op 0x2000304: Show
op 0x2000305: Show, explicit
op 0x2000306: OnActivate, explicit
op 0x2000307: ToggleBorders, tb
op 0x2000308: ToggleNavMesh
op 0x2000309: ToggleActorsPaths
op 0x200030a: SetNavMeshNumber
opcodes 0x2000308-0x3ffffff unused
opcodes 0x200030b-0x3ffffff unused

@ -1317,6 +1317,53 @@ namespace MWScript
}
};
class OpToggleNavMesh : public Interpreter::Opcode0
{
public:
virtual void execute (Interpreter::Runtime& runtime)
{
bool enabled =
MWBase::Environment::get().getWorld()->toggleRenderMode (MWRender::Render_NavMesh);
runtime.getContext().report (enabled ?
"Navigation Mesh Rendering -> On" : "Navigation Mesh Rendering -> Off");
}
};
class OpToggleActorsPaths : public Interpreter::Opcode0
{
public:
virtual void execute (Interpreter::Runtime& runtime)
{
bool enabled =
MWBase::Environment::get().getWorld()->toggleRenderMode (MWRender::Render_ActorsPaths);
runtime.getContext().report (enabled ?
"Agents Paths Rendering -> On" : "Agents Paths Rendering -> Off");
}
};
class OpSetNavMeshNumberToRender : public Interpreter::Opcode0
{
public:
virtual void execute (Interpreter::Runtime& runtime)
{
const auto navMeshNumber = runtime[0].mInteger;
runtime.pop();
if (navMeshNumber < 0)
{
runtime.getContext().report("Invalid navmesh number: use not less than zero values");
return;
}
MWBase::Environment::get().getWorld()->setNavMeshNumberToRender(static_cast<std::size_t>(navMeshNumber));
}
};
void installOpcodes (Interpreter::Interpreter& interpreter)
{
interpreter.installSegment5 (Compiler::Misc::opcodeXBox, new OpXBox);
@ -1417,6 +1464,9 @@ namespace MWScript
interpreter.installSegment3 (Compiler::Misc::opcodeShowSceneGraph, new OpShowSceneGraph<ImplicitRef>);
interpreter.installSegment3 (Compiler::Misc::opcodeShowSceneGraphExplicit, new OpShowSceneGraph<ExplicitRef>);
interpreter.installSegment5 (Compiler::Misc::opcodeToggleBorders, new OpToggleBorders);
interpreter.installSegment5 (Compiler::Misc::opcodeToggleNavMesh, new OpToggleNavMesh);
interpreter.installSegment5 (Compiler::Misc::opcodeToggleActorsPaths, new OpToggleActorsPaths);
interpreter.installSegment5 (Compiler::Misc::opcodeSetNavMeshNumberToRender, new OpSetNavMeshNumberToRender);
}
}
}

@ -265,9 +265,10 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot
writer.save (stream);
Loading::Listener& listener = *MWBase::Environment::get().getWindowManager()->getLoadingScreen();
int messagesCount = MWBase::Environment::get().getWindowManager()->getMessagesCount();
// Using only Cells for progress information, since they typically have the largest records by far
listener.setProgressRange(MWBase::Environment::get().getWorld()->countSavedGameCells());
listener.setLabel("#{sNotifyMessage4}", true);
listener.setLabel("#{sNotifyMessage4}", true, messagesCount > 0);
Loading::ScopedLoad load(&listener);
@ -389,9 +390,10 @@ void MWState::StateManager::loadGame (const Character *character, const std::str
std::map<int, int> contentFileMap = buildContentFileIndexMap (reader);
Loading::Listener& listener = *MWBase::Environment::get().getWindowManager()->getLoadingScreen();
int messagesCount = MWBase::Environment::get().getWindowManager()->getMessagesCount();
listener.setProgressRange(100);
listener.setLabel("#{sLoadingMessage14}");
listener.setLabel("#{sLoadingMessage14}", false, messagesCount > 0);
Loading::ScopedLoad load(&listener);

@ -70,6 +70,22 @@ namespace MWWorld
return mCellRef.mEnchantmentCharge;
}
float CellRef::getNormalizedEnchantmentCharge(int maxCharge) const
{
if (maxCharge == 0)
{
return 0;
}
else if (mCellRef.mEnchantmentCharge == -1)
{
return 1;
}
else
{
return mCellRef.mEnchantmentCharge / static_cast<float>(maxCharge);
}
}
void CellRef::setEnchantmentCharge(float charge)
{
if (charge != mCellRef.mEnchantmentCharge)

@ -56,6 +56,9 @@ namespace MWWorld
// Remaining enchantment charge. This could be -1 if the charge was not touched yet (i.e. full).
float getEnchantmentCharge() const;
// Remaining enchantment charge rescaled to the supplied maximum charge (such as one of the enchantment).
float getNormalizedEnchantmentCharge(int maxCharge) const;
void setEnchantmentCharge(float charge);
// For weapon or armor, this is the remaining item health.

@ -432,7 +432,7 @@ namespace MWWorld
mHasState = true;
}
int CellStore::count() const
std::size_t CellStore::count() const
{
return mMergedRefs.size();
}

@ -240,7 +240,7 @@ namespace MWWorld
ESM::FogState* getFog () const;
int count() const;
std::size_t count() const;
///< Return total number of references, including deleted ones.
void load ();
@ -283,7 +283,7 @@ namespace MWWorld
/// \attention This function also lists deleted (count 0) objects!
/// \return Iteration completed?
template<class Visitor>
bool forEachConst (Visitor& visitor) const
bool forEachConst (Visitor&& visitor) const
{
if (mState != State_Loaded)
return false;

@ -83,6 +83,18 @@ namespace MWWorld
return ptr.getCellRef().getCharge();
}
float Class::getItemNormalizedHealth (const ConstPtr& ptr) const
{
if (getItemMaxHealth(ptr) == 0)
{
return 0.f;
}
else
{
return getItemHealth(ptr) / static_cast<float>(getItemMaxHealth(ptr));
}
}
int Class::getItemMaxHealth (const ConstPtr& ptr) const
{
throw std::runtime_error ("class does not have item health");

@ -112,6 +112,9 @@ namespace MWWorld
virtual int getItemHealth (const ConstPtr& ptr) const;
///< Return current item health or throw an exception if class does not have item health
virtual float getItemNormalizedHealth (const ConstPtr& ptr) const;
///< Return current item health re-scaled to maximum health
virtual int getItemMaxHealth (const ConstPtr& ptr) const;
///< Return item max health or throw an exception, if class does not have item health
/// (default implementation: throw an exception)

@ -2,12 +2,19 @@
#include <limits>
#include <BulletCollision/CollisionDispatch/btCollisionObject.h>
#include <BulletCollision/CollisionShapes/btCompoundShape.h>
#include <BulletCollision/CollisionShapes/btHeightfieldTerrainShape.h>
#include <components/debug/debuglog.hpp>
#include <components/loadinglistener/loadinglistener.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/settings/settings.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/resource/scenemanager.hpp>
#include <components/resource/bulletshape.hpp>
#include <components/detournavigator/navigator.hpp>
#include <components/detournavigator/debug.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
@ -19,6 +26,10 @@
#include "../mwrender/landmanager.hpp"
#include "../mwphysics/physicssystem.hpp"
#include "../mwphysics/actor.hpp"
#include "../mwphysics/object.hpp"
#include "../mwphysics/heightfield.hpp"
#include "../mwphysics/convert.hpp"
#include "player.hpp"
#include "localscripts.hpp"
@ -30,25 +41,45 @@
namespace
{
osg::Quat makeActorOsgQuat(const ESM::Position& position)
{
return osg::Quat(position.rot[2], osg::Vec3(0, 0, -1));
}
void setNodeRotation(const MWWorld::Ptr& ptr, MWRender::RenderingManager& rendering, bool inverseRotationOrder)
osg::Quat makeInversedOrderObjectOsgQuat(const ESM::Position& position)
{
if (!ptr.getRefData().getBaseNode())
return;
const float xr = position.rot[0];
const float yr = position.rot[1];
const float zr = position.rot[2];
osg::Quat worldRotQuat(ptr.getRefData().getPosition().rot[2], osg::Vec3(0,0,-1));
if (!ptr.getClass().isActor())
return osg::Quat(xr, osg::Vec3(-1, 0, 0))
* osg::Quat(yr, osg::Vec3(0, -1, 0))
* osg::Quat(zr, osg::Vec3(0, 0, -1));
}
osg::Quat makeObjectOsgQuat(const ESM::Position& position)
{
float xr = ptr.getRefData().getPosition().rot[0];
float yr = ptr.getRefData().getPosition().rot[1];
if (!inverseRotationOrder)
worldRotQuat = worldRotQuat * osg::Quat(yr, osg::Vec3(0,-1,0)) *
osg::Quat(xr, osg::Vec3(-1,0,0));
else
worldRotQuat = osg::Quat(xr, osg::Vec3(-1,0,0)) * osg::Quat(yr, osg::Vec3(0,-1,0)) * worldRotQuat;
const float xr = position.rot[0];
const float yr = position.rot[1];
const float zr = position.rot[2];
return osg::Quat(zr, osg::Vec3(0, 0, -1))
* osg::Quat(yr, osg::Vec3(0, -1, 0))
* osg::Quat(xr, osg::Vec3(-1, 0, 0));
}
rendering.rotateObject(ptr, worldRotQuat);
void setNodeRotation(const MWWorld::Ptr& ptr, MWRender::RenderingManager& rendering, const bool inverseRotationOrder)
{
if (!ptr.getRefData().getBaseNode())
return;
rendering.rotateObject(ptr,
ptr.getClass().isActor()
? makeActorOsgQuat(ptr.getRefData().getPosition())
: (inverseRotationOrder
? makeInversedOrderObjectOsgQuat(ptr.getRefData().getPosition())
: makeObjectOsgQuat(ptr.getRefData().getPosition()))
);
}
void addObject(const MWWorld::Ptr& ptr, MWPhysics::PhysicsSystem& physics,
@ -84,6 +115,73 @@ namespace
MWBase::Environment::get().getWorld()->applyLoopingParticles(ptr);
}
void addObject(const MWWorld::Ptr& ptr, const MWPhysics::PhysicsSystem& physics, DetourNavigator::Navigator& navigator)
{
if (const auto object = physics.getObject(ptr))
{
if (ptr.getClass().isDoor() && !ptr.getCellRef().getTeleport())
{
const auto shape = object->getShapeInstance()->getCollisionShape();
btVector3 aabbMin;
btVector3 aabbMax;
shape->getAabb(btTransform::getIdentity(), aabbMin, aabbMax);
const auto center = (aabbMax + aabbMin) * 0.5f;
const auto distanceFromDoor = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 0.5f;
const auto toPoint = aabbMax.x() - aabbMin.x() < aabbMax.y() - aabbMin.y()
? btVector3(distanceFromDoor, 0, 0)
: btVector3(0, distanceFromDoor, 0);
const auto& transform = object->getCollisionObject()->getWorldTransform();
const btTransform closedDoorTransform(
MWPhysics::toBullet(makeObjectOsgQuat(ptr.getCellRef().getPosition())),
transform.getOrigin()
);
const auto start = DetourNavigator::makeOsgVec3f(closedDoorTransform(center + toPoint));
const auto startPoint = physics.castRay(start, start - osg::Vec3f(0, 0, 1000), ptr, {},
MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Water);
const auto connectionStart = startPoint.mHit ? startPoint.mHitPos : start;
const auto end = DetourNavigator::makeOsgVec3f(closedDoorTransform(center - toPoint));
const auto endPoint = physics.castRay(end, end - osg::Vec3f(0, 0, 1000), ptr, {},
MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Water);
const auto connectionEnd = endPoint.mHit ? endPoint.mHitPos : end;
navigator.addObject(
DetourNavigator::ObjectId(object),
DetourNavigator::DoorShapes(
*shape,
object->getShapeInstance()->getAvoidCollisionShape(),
connectionStart,
connectionEnd
),
transform
);
}
else
{
navigator.addObject(
DetourNavigator::ObjectId(object),
DetourNavigator::ObjectShapes {
*object->getShapeInstance()->getCollisionShape(),
object->getShapeInstance()->getAvoidCollisionShape()
},
object->getCollisionObject()->getWorldTransform()
);
}
}
else if (const auto actor = physics.getActor(ptr))
{
const auto halfExtents = ptr.getCell()->isExterior()
? physics.getHalfExtents(MWBase::Environment::get().getWorld()->getPlayerPtr())
: actor->getHalfExtents();
navigator.addAgent(halfExtents);
}
}
void updateObjectRotation (const MWWorld::Ptr& ptr, MWPhysics::PhysicsSystem& physics,
MWRender::RenderingManager& rendering, bool inverseRotationOrder)
{
@ -110,24 +208,19 @@ namespace
MWWorld::CellStore& mCell;
bool mRescale;
Loading::Listener& mLoadingListener;
MWPhysics::PhysicsSystem& mPhysics;
MWRender::RenderingManager& mRendering;
std::vector<MWWorld::Ptr> mToInsert;
InsertVisitor (MWWorld::CellStore& cell, bool rescale, Loading::Listener& loadingListener,
MWPhysics::PhysicsSystem& physics, MWRender::RenderingManager& rendering);
InsertVisitor (MWWorld::CellStore& cell, bool rescale, Loading::Listener& loadingListener);
bool operator() (const MWWorld::Ptr& ptr);
void insert();
template <class AddObject>
void insert(AddObject&& addObject);
};
InsertVisitor::InsertVisitor (MWWorld::CellStore& cell, bool rescale,
Loading::Listener& loadingListener, MWPhysics::PhysicsSystem& physics,
MWRender::RenderingManager& rendering)
: mCell (cell), mRescale (rescale), mLoadingListener (loadingListener),
mPhysics (physics),
mRendering (rendering)
InsertVisitor::InsertVisitor (MWWorld::CellStore& cell, bool rescale, Loading::Listener& loadingListener)
: mCell (cell), mRescale (rescale), mLoadingListener (loadingListener)
{}
bool InsertVisitor::operator() (const MWWorld::Ptr& ptr)
@ -138,7 +231,8 @@ namespace
return true;
}
void InsertVisitor::insert()
template <class AddObject>
void InsertVisitor::insert(AddObject&& addObject)
{
for (std::vector<MWWorld::Ptr>::iterator it = mToInsert.begin(); it != mToInsert.end(); ++it)
{
@ -155,7 +249,7 @@ namespace
{
try
{
addObject(ptr, mPhysics, mRendering);
addObject(ptr);
}
catch (const std::exception& e)
{
@ -178,6 +272,11 @@ namespace
}
};
int getCellPositionDistanceToOrigin(const std::pair<int, int>& cellPosition)
{
return std::abs(cellPosition.first) + std::abs(cellPosition.second);
}
}
@ -233,14 +332,28 @@ namespace MWWorld
void Scene::unloadCell (CellStoreCollection::iterator iter)
{
Log(Debug::Info) << "Unloading cell " << (*iter)->getCell()->getDescription();
DetourNavigator::log("unload cell ", (*iter)->getCell()->getDescription());
const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
ListAndResetObjectsVisitor visitor;
(*iter)->forEach<ListAndResetObjectsVisitor>(visitor);
for (std::vector<MWWorld::Ptr>::const_iterator iter2 (visitor.mObjects.begin());
iter2!=visitor.mObjects.end(); ++iter2)
const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
const auto playerHalfExtents = mPhysics->getHalfExtents(player);
for (const auto& ptr : visitor.mObjects)
{
if (const auto object = mPhysics->getObject(ptr))
navigator->removeObject(DetourNavigator::ObjectId(object));
else if (const auto actor = mPhysics->getActor(ptr))
{
mPhysics->remove(*iter2);
navigator->removeAgent(ptr.getCell()->isExterior() ? playerHalfExtents : actor->getHalfExtents());
mRendering.removeActorPath(ptr);
}
mPhysics->remove(ptr);
}
const auto cellX = (*iter)->getCell()->getGridX();
const auto cellY = (*iter)->getCell()->getGridY();
if ((*iter)->getCell()->isExterior())
{
@ -250,9 +363,18 @@ namespace MWWorld
(*iter)->getCell()->getGridY()
);
if (land && land->mDataTypes&ESM::Land::DATA_VHGT)
mPhysics->removeHeightField ((*iter)->getCell()->getGridX(), (*iter)->getCell()->getGridY());
{
if (const auto heightField = mPhysics->getHeightField(cellX, cellY))
navigator->removeObject(DetourNavigator::ObjectId(heightField));
mPhysics->removeHeightField(cellX, cellY);
}
}
if ((*iter)->getCell()->hasWater())
navigator->removeWater(osg::Vec2i(cellX, cellY));
navigator->update(player.getRefData().getPosition().asVec3());
MWBase::Environment::get().getMechanicsManager()->drop (*iter);
mRendering.removeCell(*iter);
@ -271,20 +393,24 @@ namespace MWWorld
if(result.second)
{
Log(Debug::Info) << "Loading cell " << cell->getCell()->getDescription();
DetourNavigator::log("load cell ", cell->getCell()->getDescription());
float verts = ESM::Land::LAND_SIZE;
float worldsize = ESM::Land::REAL_SIZE;
const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
const int cellX = cell->getCell()->getGridX();
const int cellY = cell->getCell()->getGridY();
// Load terrain physics first...
if (cell->getCell()->isExterior())
{
int cellX = cell->getCell()->getGridX();
int cellY = cell->getCell()->getGridY();
osg::ref_ptr<const ESMTerrain::LandObject> land = mRendering.getLandManager()->getLand(cellX, cellY);
const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VHGT) : 0;
if (data)
{
mPhysics->addHeightField (data->mHeights, cellX, cell->getCell()->getGridY(), worldsize / (verts-1), verts, data->mMinHeight, data->mMaxHeight, land.get());
mPhysics->addHeightField (data->mHeights, cellX, cellY, worldsize / (verts-1), verts, data->mMinHeight, data->mMaxHeight, land.get());
}
else
{
@ -292,6 +418,10 @@ namespace MWWorld
defaultHeight.resize(verts*verts, ESM::Land::DEFAULT_HEIGHT);
mPhysics->addHeightField (&defaultHeight[0], cell->getCell()->getGridX(), cell->getCell()->getGridY(), worldsize / (verts-1), verts, ESM::Land::DEFAULT_HEIGHT, ESM::Land::DEFAULT_HEIGHT, land.get());
}
if (const auto heightField = mPhysics->getHeightField(cellX, cellY))
navigator->addObject(DetourNavigator::ObjectId(heightField), *heightField->getShape(),
heightField->getCollisionObject()->getWorldTransform());
}
// register local scripts
@ -313,10 +443,25 @@ namespace MWWorld
{
mPhysics->enableWater(waterLevel);
mRendering.setWaterHeight(waterLevel);
if (cell->getCell()->isExterior())
{
if (const auto heightField = mPhysics->getHeightField(cellX, cellY))
navigator->addWater(osg::Vec2i(cellX, cellY), ESM::Land::REAL_SIZE,
cell->getWaterLevel(), heightField->getCollisionObject()->getWorldTransform());
}
else
{
navigator->addWater(osg::Vec2i(cellX, cellY), std::numeric_limits<int>::max(),
cell->getWaterLevel(), btTransform::getIdentity());
}
}
else
mPhysics->disableWater();
const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
navigator->update(player.getRefData().getPosition().asVec3());
if (!cell->isExterior() && !(cell->getCell()->mData.mFlags & ESM::Cell::QuasiEx))
mRendering.configureAmbient(cell->getCell());
}
@ -337,6 +482,10 @@ namespace MWWorld
void Scene::playerMoved(const osg::Vec3f &pos)
{
const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
navigator->update(player.getRefData().getPosition().asVec3());
if (!mCurrentCell || !mCurrentCell->isExterior())
return;
@ -355,21 +504,22 @@ namespace MWWorld
}
}
void Scene::changeCellGrid (int X, int Y, bool changeEvent)
void Scene::changeCellGrid (int playerCellX, int playerCellY, bool changeEvent)
{
Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen();
Loading::ScopedLoad load(loadingListener);
int messagesCount = MWBase::Environment::get().getWindowManager()->getMessagesCount();
std::string loadingExteriorText = "#{sLoadingMessage3}";
loadingListener->setLabel(loadingExteriorText);
loadingListener->setLabel(loadingExteriorText, false, messagesCount > 0);
CellStoreCollection::iterator active = mActiveCells.begin();
while (active!=mActiveCells.end())
{
if ((*active)->getCell()->isExterior())
{
if (std::abs (X-(*active)->getCell()->getGridX())<=mHalfGridSize &&
std::abs (Y-(*active)->getCell()->getGridY())<=mHalfGridSize)
if (std::abs (playerCellX-(*active)->getCell()->getGridX())<=mHalfGridSize &&
std::abs (playerCellY-(*active)->getCell()->getGridY())<=mHalfGridSize)
{
// keep cells within the new grid
++active;
@ -379,11 +529,12 @@ namespace MWWorld
unloadCell (active++);
}
int refsToLoad = 0;
std::size_t refsToLoad = 0;
std::vector<std::pair<int, int>> cellsPositionsToLoad;
// get the number of refs to load
for (int x=X-mHalfGridSize; x<=X+mHalfGridSize; ++x)
for (int x = playerCellX - mHalfGridSize; x <= playerCellX + mHalfGridSize; ++x)
{
for (int y=Y-mHalfGridSize; y<=Y+mHalfGridSize; ++y)
for (int y = playerCellY - mHalfGridSize; y <= playerCellY + mHalfGridSize; ++y)
{
CellStoreCollection::iterator iter = mActiveCells.begin();
@ -399,40 +550,58 @@ namespace MWWorld
}
if (iter==mActiveCells.end())
{
refsToLoad += MWBase::Environment::get().getWorld()->getExterior(x, y)->count();
cellsPositionsToLoad.push_back(std::make_pair(x, y));
}
}
}
loadingListener->setProgressRange(refsToLoad);
// Load cells
for (int x=X-mHalfGridSize; x<=X+mHalfGridSize; ++x)
const auto getDistanceToPlayerCell = [&] (const std::pair<int, int>& cellPosition)
{
return std::abs(cellPosition.first - playerCellX) + std::abs(cellPosition.second - playerCellY);
};
const auto getCellPositionPriority = [&] (const std::pair<int, int>& cellPosition)
{
for (int y=Y-mHalfGridSize; y<=Y+mHalfGridSize; ++y)
return std::make_pair(getDistanceToPlayerCell(cellPosition), getCellPositionDistanceToOrigin(cellPosition));
};
std::sort(cellsPositionsToLoad.begin(), cellsPositionsToLoad.end(),
[&] (const std::pair<int, int>& lhs, const std::pair<int, int>& rhs) {
return getCellPositionPriority(lhs) < getCellPositionPriority(rhs);
});
// Load cells
for (const auto& cellPosition : cellsPositionsToLoad)
{
const auto x = cellPosition.first;
const auto y = cellPosition.second;
CellStoreCollection::iterator iter = mActiveCells.begin();
while (iter!=mActiveCells.end())
while (iter != mActiveCells.end())
{
assert ((*iter)->getCell()->isExterior());
if (x==(*iter)->getCell()->getGridX() &&
y==(*iter)->getCell()->getGridY())
if (x == (*iter)->getCell()->getGridX() &&
y == (*iter)->getCell()->getGridY())
break;
++iter;
}
if (iter==mActiveCells.end())
if (iter == mActiveCells.end())
{
CellStore *cell = MWBase::Environment::get().getWorld()->getExterior(x, y);
loadCell (cell, loadingListener, changeEvent);
}
}
}
CellStore* current = MWBase::Environment::get().getWorld()->getExterior(X,Y);
CellStore* current = MWBase::Environment::get().getWorld()->getExterior(playerCellX, playerCellY);
MWBase::Environment::get().getWindowManager()->changeCell(current);
if (changeEvent)
@ -476,8 +645,9 @@ namespace MWWorld
mLastPlayerPos = pos.asVec3();
}
Scene::Scene (MWRender::RenderingManager& rendering, MWPhysics::PhysicsSystem *physics)
: mCurrentCell (0), mCellChanged (false), mPhysics(physics), mRendering(rendering)
Scene::Scene (MWRender::RenderingManager& rendering, MWPhysics::PhysicsSystem *physics,
DetourNavigator::Navigator& navigator)
: mCurrentCell (0), mCellChanged (false), mPhysics(physics), mRendering(rendering), mNavigator(navigator)
, mPreloadTimer(0.f)
, mHalfGridSize(Settings::Manager::getInt("exterior cell load distance", "Cells"))
, mCellLoadingThreshold(1024.f)
@ -526,8 +696,9 @@ namespace MWWorld
MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.5);
Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen();
int messagesCount = MWBase::Environment::get().getWindowManager()->getMessagesCount();
std::string loadingInteriorText = "#{sLoadingMessage2}";
loadingListener->setLabel(loadingInteriorText);
loadingListener->setLabel(loadingInteriorText, false, messagesCount > 0);
Loading::ScopedLoad load(loadingListener);
if(!loadcell)
@ -553,8 +724,7 @@ namespace MWWorld
while (active!=mActiveCells.end())
unloadCell (active++);
int refsToLoad = cell->count();
loadingListener->setProgressRange(refsToLoad);
loadingListener->setProgressRange(cell->count());
// Load cell.
loadCell (cell, loadingListener, changeEvent);
@ -606,9 +776,10 @@ namespace MWWorld
void Scene::insertCell (CellStore &cell, bool rescale, Loading::Listener* loadingListener)
{
InsertVisitor insertVisitor (cell, rescale, *loadingListener, *mPhysics, mRendering);
InsertVisitor insertVisitor (cell, rescale, *loadingListener);
cell.forEach (insertVisitor);
insertVisitor.insert();
insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mRendering); });
insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mNavigator); });
// do adjustPosition (snapping actors to ground) after objects are loaded, so we don't depend on the loading order
AdjustPositionVisitor adjustPosVisitor;
@ -620,7 +791,11 @@ namespace MWWorld
try
{
addObject(ptr, *mPhysics, mRendering);
addObject(ptr, *mPhysics, mNavigator);
MWBase::Environment::get().getWorld()->scaleObject(ptr, ptr.getCellRef().getScale());
const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
navigator->update(player.getRefData().getPosition().asVec3());
}
catch (std::exception& e)
{
@ -632,6 +807,20 @@ namespace MWWorld
{
MWBase::Environment::get().getMechanicsManager()->remove (ptr);
MWBase::Environment::get().getSoundManager()->stopSound3D (ptr);
const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
if (const auto object = mPhysics->getObject(ptr))
{
navigator->removeObject(DetourNavigator::ObjectId(object));
const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
navigator->update(player.getRefData().getPosition().asVec3());
}
else if (const auto actor = mPhysics->getActor(ptr))
{
const auto& halfExtents = ptr.getCell()->isExterior()
? mPhysics->getHalfExtents(MWBase::Environment::get().getWorld()->getPlayerPtr())
: actor->getHalfExtents();
navigator->removeAgent(halfExtents);
}
mPhysics->remove(ptr);
mRendering.removeObject (ptr);
if (ptr.getClass().isActor())

@ -6,6 +6,7 @@
#include <set>
#include <memory>
#include <unordered_map>
namespace osg
{
@ -27,6 +28,12 @@ namespace Loading
class Listener;
}
namespace DetourNavigator
{
class Navigator;
class Water;
}
namespace MWRender
{
class SkyManager;
@ -57,6 +64,7 @@ namespace MWWorld
bool mCellChanged;
MWPhysics::PhysicsSystem *mPhysics;
MWRender::RenderingManager& mRendering;
DetourNavigator::Navigator& mNavigator;
std::unique_ptr<CellPreloader> mPreloader;
float mPreloadTimer;
int mHalfGridSize;
@ -74,7 +82,7 @@ namespace MWWorld
void insertCell (CellStore &cell, bool rescale, Loading::Listener* loadingListener);
// Load and unload cells as necessary to create a cell grid with "X" and "Y" in the center
void changeCellGrid (int X, int Y, bool changeEvent = true);
void changeCellGrid (int playerCellX, int playerCellY, bool changeEvent = true);
void getGridCenter(int& cellX, int& cellY);
@ -85,7 +93,8 @@ namespace MWWorld
public:
Scene (MWRender::RenderingManager& rendering, MWPhysics::PhysicsSystem *physics);
Scene (MWRender::RenderingManager& rendering, MWPhysics::PhysicsSystem *physics,
DetourNavigator::Navigator& navigator);
~Scene();

@ -3,6 +3,9 @@
#include <osg/Group>
#include <osg/ComputeBoundsVisitor>
#include <BulletCollision/CollisionDispatch/btCollisionWorld.h>
#include <BulletCollision/CollisionShapes/btCompoundShape.h>
#include <components/debug/debuglog.hpp>
#include <components/esm/esmreader.hpp>
@ -15,10 +18,15 @@
#include <components/files/collections.hpp>
#include <components/resource/bulletshape.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/sceneutil/positionattitudetransform.hpp>
#include <components/detournavigator/debug.hpp>
#include <components/detournavigator/navigator.hpp>
#include <components/detournavigator/debug.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/soundmanager.hpp"
#include "../mwbase/mechanicsmanager.hpp"
@ -47,6 +55,7 @@
#include "../mwphysics/physicssystem.hpp"
#include "../mwphysics/actor.hpp"
#include "../mwphysics/collisiontype.hpp"
#include "../mwphysics/object.hpp"
#include "player.hpp"
#include "manualref.hpp"
@ -159,12 +168,6 @@ namespace MWWorld
mLevitationEnabled(true), mGoToJail(false), mDaysInPrison(0),
mPlayerTraveling(false), mPlayerInJail(false), mSpellPreloadTimer(0.f)
{
mPhysics.reset(new MWPhysics::PhysicsSystem(resourceSystem, rootNode));
mRendering.reset(new MWRender::RenderingManager(viewer, rootNode, resourceSystem, workQueue, &mFallback, resourcePath));
mProjectileManager.reset(new ProjectileManager(mRendering->getLightRoot(), resourceSystem, mRendering.get(), mPhysics.get()));
mRendering->preloadCommonAssets();
mEsm.resize(contentFiles.size());
Loading::Listener* listener = MWBase::Environment::get().getWindowManager()->getLoadingScreen();
listener->loadingOn();
@ -193,9 +196,49 @@ namespace MWWorld
mSwimHeightScale = mStore.get<ESM::GameSetting>().find("fSwimHeightScale")->mValue.getFloat();
mPhysics.reset(new MWPhysics::PhysicsSystem(resourceSystem, rootNode));
DetourNavigator::Settings navigatorSettings;
navigatorSettings.mBorderSize = Settings::Manager::getInt("border size", "Navigator");
navigatorSettings.mCellHeight = Settings::Manager::getFloat("cell height", "Navigator");
navigatorSettings.mCellSize = Settings::Manager::getFloat("cell size", "Navigator");
navigatorSettings.mDetailSampleDist = Settings::Manager::getFloat("detail sample dist", "Navigator");
navigatorSettings.mDetailSampleMaxError = Settings::Manager::getFloat("detail sample max error", "Navigator");
navigatorSettings.mMaxClimb = MWPhysics::sStepSizeUp;
navigatorSettings.mMaxSimplificationError = Settings::Manager::getFloat("max simplification error", "Navigator");
navigatorSettings.mMaxSlope = MWPhysics::sMaxSlope;
navigatorSettings.mRecastScaleFactor = Settings::Manager::getFloat("recast scale factor", "Navigator");
navigatorSettings.mSwimHeightScale = mSwimHeightScale;
navigatorSettings.mMaxEdgeLen = Settings::Manager::getInt("max edge len", "Navigator");
navigatorSettings.mMaxNavMeshQueryNodes = Settings::Manager::getInt("max nav mesh query nodes", "Navigator");
navigatorSettings.mMaxPolys = Settings::Manager::getInt("max polygons per tile", "Navigator");
navigatorSettings.mMaxVertsPerPoly = Settings::Manager::getInt("max verts per poly", "Navigator");
navigatorSettings.mRegionMergeSize = Settings::Manager::getInt("region merge size", "Navigator");
navigatorSettings.mRegionMinSize = Settings::Manager::getInt("region min size", "Navigator");
navigatorSettings.mTileSize = Settings::Manager::getInt("tile size", "Navigator");
navigatorSettings.mAsyncNavMeshUpdaterThreads = static_cast<std::size_t>(Settings::Manager::getInt("async nav mesh updater threads", "Navigator"));
navigatorSettings.mMaxNavMeshTilesCacheSize = static_cast<std::size_t>(Settings::Manager::getInt("max nav mesh tiles cache size", "Navigator"));
navigatorSettings.mMaxPolygonPathSize = static_cast<std::size_t>(Settings::Manager::getInt("max polygon path size", "Navigator"));
navigatorSettings.mMaxSmoothPathSize = static_cast<std::size_t>(Settings::Manager::getInt("max smooth path size", "Navigator"));
navigatorSettings.mTrianglesPerChunk = static_cast<std::size_t>(Settings::Manager::getInt("triangles per chunk", "Navigator"));
navigatorSettings.mEnableWriteRecastMeshToFile = Settings::Manager::getBool("enable write recast mesh to file", "Navigator");
navigatorSettings.mEnableWriteNavMeshToFile = Settings::Manager::getBool("enable write nav mesh to file", "Navigator");
navigatorSettings.mRecastMeshPathPrefix = Settings::Manager::getString("recast mesh path prefix", "Navigator");
navigatorSettings.mNavMeshPathPrefix = Settings::Manager::getString("nav mesh path prefix", "Navigator");
navigatorSettings.mEnableRecastMeshFileNameRevision = Settings::Manager::getBool("enable recast mesh file name revision", "Navigator");
navigatorSettings.mEnableNavMeshFileNameRevision = Settings::Manager::getBool("enable nav mesh file name revision", "Navigator");
if (Settings::Manager::getBool("enable log", "Navigator"))
DetourNavigator::Log::instance().setSink(std::unique_ptr<DetourNavigator::FileSink>(
new DetourNavigator::FileSink(Settings::Manager::getString("log path", "Navigator"))));
mNavigator.reset(new DetourNavigator::Navigator(navigatorSettings));
mRendering.reset(new MWRender::RenderingManager(viewer, rootNode, resourceSystem, workQueue, &mFallback, resourcePath, *mNavigator));
mProjectileManager.reset(new ProjectileManager(mRendering->getLightRoot(), resourceSystem, mRendering.get(), mPhysics.get()));
mRendering->preloadCommonAssets();
mWeatherManager.reset(new MWWorld::WeatherManager(*mRendering, mFallback, mStore));
mWorldScene.reset(new Scene(*mRendering.get(), mPhysics.get()));
mWorldScene.reset(new Scene(*mRendering.get(), mPhysics.get(), *mNavigator));
}
void World::fillGlobalVariables()
@ -1506,6 +1549,32 @@ namespace MWWorld
moveObjectImp(player->first, player->second.x(), player->second.y(), player->second.z(), false);
}
void World::updateNavigator()
{
bool updated = false;
mPhysics->forEachAnimatedObject([&] (const MWPhysics::Object* object)
{
updated = updateNavigatorObject(object) || updated;
});
for (const auto& door : mDoorStates)
if (const auto object = mPhysics->getObject(door.first))
updated = updateNavigatorObject(object) || updated;
if (updated)
mNavigator->update(getPlayerPtr().getRefData().getPosition().asVec3());
}
bool World::updateNavigatorObject(const MWPhysics::Object* object)
{
const DetourNavigator::ObjectShapes shapes {
*object->getShapeInstance()->getCollisionShape(),
object->getShapeInstance()->getAvoidCollisionShape()
};
return mNavigator->updateObject(DetourNavigator::ObjectId(object), shapes, object->getCollisionObject()->getWorldTransform());
}
bool World::castRay (float x1, float y1, float z1, float x2, float y2, float z2)
{
int mask = MWPhysics::CollisionType_World | MWPhysics::CollisionType_Door;
@ -1583,6 +1652,16 @@ namespace MWWorld
}
}
void World::setActorCollisionMode(const MWWorld::Ptr& ptr, bool enabled)
{
mPhysics->setActorCollisionMode(ptr, enabled);
}
bool World::isActorCollisionEnabled(const MWWorld::Ptr& ptr)
{
return mPhysics->isActorCollisionEnabled(ptr);
}
bool World::toggleCollisionMode()
{
if (mPhysics->toggleCollisionMode())
@ -1698,7 +1777,10 @@ namespace MWWorld
updateWeather(duration, paused);
if (!paused)
{
doPhysics (duration);
updateNavigator();
}
updatePlayer();
@ -2295,6 +2377,7 @@ namespace MWWorld
{
// Remove the old CharacterController
MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr());
mNavigator->removeAgent(mPhysics->getHalfExtents(getPlayerPtr()));
mPhysics->remove(getPlayerPtr());
mRendering->removePlayer(getPlayerPtr());
@ -2329,6 +2412,8 @@ namespace MWWorld
mPhysics->addActor(getPlayerPtr(), model);
applyLoopingParticles(player);
mNavigator->addAgent(mPhysics->getHalfExtents(getPlayerPtr()));
}
World::RestPermitted World::canRest () const
@ -3560,8 +3645,14 @@ namespace MWWorld
MWBase::Environment::get().getMechanicsManager()->getObjectsInRange(
origin, feetToGameUnits(static_cast<float>(effectIt->mArea)), objects);
for (std::vector<MWWorld::Ptr>::iterator affected = objects.begin(); affected != objects.end(); ++affected)
{
// Ignore actors without collisions here, otherwise it will be possible to hit actors outside processing range.
if (affected->getClass().isActor() && !isActorCollisionEnabled(*affected))
continue;
toApply[*affected].push_back(*effectIt);
}
}
// Now apply the appropriate effects to each actor in range
for (std::map<MWWorld::Ptr, std::vector<ESM::ENAMstruct> >::iterator apply = toApply.begin(); apply != toApply.end(); ++apply)
@ -3688,4 +3779,20 @@ namespace MWWorld
}
}
DetourNavigator::Navigator* World::getNavigator() const
{
return mNavigator.get();
}
void World::updateActorPath(const MWWorld::ConstPtr& actor, const std::deque<osg::Vec3f>& path,
const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const
{
mRendering->updateActorPath(actor, path, halfExtents, start, end);
}
void World::setNavMeshNumberToRender(const std::size_t value)
{
mRendering->setNavMeshNumber(value);
}
}

@ -61,6 +61,11 @@ namespace ToUTF8
struct ContentLoader;
namespace MWPhysics
{
class Object;
}
namespace MWWorld
{
class WeatherManager;
@ -94,6 +99,7 @@ namespace MWWorld
std::unique_ptr<MWWorld::Player> mPlayer;
std::unique_ptr<MWPhysics::PhysicsSystem> mPhysics;
std::unique_ptr<DetourNavigator::Navigator> mNavigator;
std::unique_ptr<MWRender::RenderingManager> mRendering;
std::unique_ptr<MWWorld::Scene> mWorldScene;
std::unique_ptr<MWWorld::WeatherManager> mWeatherManager;
@ -147,6 +153,10 @@ namespace MWWorld
void doPhysics(float duration);
///< Run physics simulation and modify \a world accordingly.
void updateNavigator();
bool updateNavigatorObject(const MWPhysics::Object* object);
void ensureNeededRecords();
void fillGlobalVariables();
@ -407,6 +417,9 @@ namespace MWWorld
bool castRay (float x1, float y1, float z1, float x2, float y2, float z2) override;
void setActorCollisionMode(const Ptr& ptr, bool enabled) override;
bool isActorCollisionEnabled(const Ptr& ptr) override;
bool toggleCollisionMode() override;
///< Toggle collision mode for player. If disabled player object should ignore
/// collisions and gravity.
@ -690,6 +703,13 @@ namespace MWWorld
/// Preload VFX associated with this effect list
void preloadEffects(const ESM::EffectList* effectList) override;
DetourNavigator::Navigator* getNavigator() const override;
void updateActorPath(const MWWorld::ConstPtr& actor, const std::deque<osg::Vec3f>& path,
const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const override;
void setNavMeshNumberToRender(const std::size_t value) override;
};
}

@ -17,10 +17,21 @@ if (GTEST_FOUND AND GMOCK_FOUND)
misc/test_stringops.cpp
nifloader/testbulletnifloader.cpp
detournavigator/navigator.cpp
detournavigator/settingsutils.cpp
detournavigator/recastmeshbuilder.cpp
detournavigator/gettilespositions.cpp
detournavigator/recastmeshobject.cpp
detournavigator/navmeshtilescache.cpp
)
source_group(apps\\openmw_test_suite FILES openmw_test_suite.cpp ${UNITTEST_SRC_FILES})
find_package(RecastNavigation COMPONENTS DebugUtils Detour Recast REQUIRED)
include_directories(SYSTEM ${RecastNavigation_INCLUDE_DIRS})
openmw_add_executable(openmw_test_suite openmw_test_suite.cpp ${UNITTEST_SRC_FILES})
target_link_libraries(openmw_test_suite ${GMOCK_LIBRARIES} components)

@ -0,0 +1,104 @@
#include <components/detournavigator/gettilespositions.hpp>
#include <components/detournavigator/debug.hpp>
#include <gtest/gtest.h>
#include <gmock/gmock.h>
namespace
{
using namespace testing;
using namespace DetourNavigator;
struct CollectTilesPositions
{
std::vector<TilePosition>& mTilesPositions;
void operator ()(const TilePosition& value)
{
mTilesPositions.push_back(value);
}
};
struct DetourNavigatorGetTilesPositionsTest : Test
{
Settings mSettings;
std::vector<TilePosition> mTilesPositions;
CollectTilesPositions mCollect {mTilesPositions};
DetourNavigatorGetTilesPositionsTest()
{
mSettings.mBorderSize = 0;
mSettings.mCellSize = 0.5;
mSettings.mRecastScaleFactor = 1;
mSettings.mTileSize = 64;
}
};
TEST_F(DetourNavigatorGetTilesPositionsTest, for_object_in_single_tile_should_return_one_tile)
{
getTilesPositions(osg::Vec3f(2, 2, 0), osg::Vec3f(31, 31, 1), mSettings, mCollect);
EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0)));
}
TEST_F(DetourNavigatorGetTilesPositionsTest, for_object_with_x_bounds_in_two_tiles_should_return_two_tiles)
{
getTilesPositions(osg::Vec3f(0, 0, 0), osg::Vec3f(32, 31, 1), mSettings, mCollect);
EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0), TilePosition(1, 0)));
}
TEST_F(DetourNavigatorGetTilesPositionsTest, for_object_with_y_bounds_in_two_tiles_should_return_two_tiles)
{
getTilesPositions(osg::Vec3f(0, 0, 0), osg::Vec3f(31, 32, 1), mSettings, mCollect);
EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0), TilePosition(0, 1)));
}
TEST_F(DetourNavigatorGetTilesPositionsTest, tiling_works_only_for_x_and_y_coordinates)
{
getTilesPositions(osg::Vec3f(0, 0, 0), osg::Vec3f(31, 31, 32), mSettings, mCollect);
EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0)));
}
TEST_F(DetourNavigatorGetTilesPositionsTest, tiling_should_work_with_negative_coordinates)
{
getTilesPositions(osg::Vec3f(-31, -31, 0), osg::Vec3f(31, 31, 1), mSettings, mCollect);
EXPECT_THAT(mTilesPositions, ElementsAre(
TilePosition(-1, -1),
TilePosition(-1, 0),
TilePosition(0, -1),
TilePosition(0, 0)
));
}
TEST_F(DetourNavigatorGetTilesPositionsTest, border_size_should_extend_tile_bounds)
{
mSettings.mBorderSize = 1;
getTilesPositions(osg::Vec3f(0, 0, 0), osg::Vec3f(31.5, 31.5, 1), mSettings, mCollect);
EXPECT_THAT(mTilesPositions, ElementsAre(
TilePosition(-1, -1),
TilePosition(-1, 0),
TilePosition(-1, 1),
TilePosition(0, -1),
TilePosition(0, 0),
TilePosition(0, 1),
TilePosition(1, -1),
TilePosition(1, 0),
TilePosition(1, 1)
));
}
TEST_F(DetourNavigatorGetTilesPositionsTest, should_apply_recast_scale_factor)
{
mSettings.mRecastScaleFactor = 0.5;
getTilesPositions(osg::Vec3f(0, 0, 0), osg::Vec3f(32, 32, 1), mSettings, mCollect);
EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0)));
}
}

@ -0,0 +1,661 @@
#include "operators.hpp"
#include <components/detournavigator/navigator.hpp>
#include <components/detournavigator/exceptions.hpp>
#include <BulletCollision/CollisionShapes/btHeightfieldTerrainShape.h>
#include <BulletCollision/CollisionShapes/btBoxShape.h>
#include <BulletCollision/CollisionShapes/btCompoundShape.h>
#include <gtest/gtest.h>
#include <iterator>
#include <deque>
namespace
{
using namespace testing;
using namespace DetourNavigator;
struct DetourNavigatorNavigatorTest : Test
{
Settings mSettings;
std::unique_ptr<Navigator> mNavigator;
osg::Vec3f mPlayerPosition;
osg::Vec3f mAgentHalfExtents;
osg::Vec3f mStart;
osg::Vec3f mEnd;
std::deque<osg::Vec3f> mPath;
std::back_insert_iterator<std::deque<osg::Vec3f>> mOut;
DetourNavigatorNavigatorTest()
: mPlayerPosition(0, 0, 0)
, mAgentHalfExtents(29, 29, 66)
, mStart(-215, 215, 1)
, mEnd(215, -215, 1)
, mOut(mPath)
{
mSettings.mEnableWriteRecastMeshToFile = false;
mSettings.mEnableWriteNavMeshToFile = false;
mSettings.mEnableRecastMeshFileNameRevision = false;
mSettings.mEnableNavMeshFileNameRevision = false;
mSettings.mBorderSize = 16;
mSettings.mCellHeight = 0.2f;
mSettings.mCellSize = 0.2f;
mSettings.mDetailSampleDist = 6;
mSettings.mDetailSampleMaxError = 1;
mSettings.mMaxClimb = 34;
mSettings.mMaxSimplificationError = 1.3f;
mSettings.mMaxSlope = 49;
mSettings.mRecastScaleFactor = 0.017647058823529415f;
mSettings.mSwimHeightScale = 0.89999997615814208984375f;
mSettings.mMaxEdgeLen = 12;
mSettings.mMaxNavMeshQueryNodes = 2048;
mSettings.mMaxVertsPerPoly = 6;
mSettings.mRegionMergeSize = 20;
mSettings.mRegionMinSize = 8;
mSettings.mTileSize = 64;
mSettings.mAsyncNavMeshUpdaterThreads = 1;
mSettings.mMaxNavMeshTilesCacheSize = 1024 * 1024;
mSettings.mMaxPolygonPathSize = 1024;
mSettings.mMaxSmoothPathSize = 1024;
mSettings.mTrianglesPerChunk = 256;
mSettings.mMaxPolys = 4096;
mNavigator.reset(new Navigator(mSettings));
}
};
TEST_F(DetourNavigatorNavigatorTest, find_path_for_empty_should_throw_exception)
{
EXPECT_THROW(mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, mOut), InvalidArgument);
}
TEST_F(DetourNavigatorNavigatorTest, find_path_for_existing_agent_with_no_navmesh_should_throw_exception)
{
mNavigator->addAgent(mAgentHalfExtents);
EXPECT_THROW(mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, mOut), NavigatorException);
}
TEST_F(DetourNavigatorNavigatorTest, find_path_for_removed_agent_should_throw_exception)
{
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->removeAgent(mAgentHalfExtents);
EXPECT_THROW(mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, mOut), InvalidArgument);
}
TEST_F(DetourNavigatorNavigatorTest, add_agent_should_count_each_agent)
{
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->removeAgent(mAgentHalfExtents);
EXPECT_THROW(mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, mOut), NavigatorException);
}
TEST_F(DetourNavigatorNavigatorTest, update_then_find_path_should_return_path)
{
const std::array<btScalar, 5 * 5> heightfieldData {{
0, 0, 0, 0, 0,
0, -25, -25, -25, -25,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
}};
btHeightfieldTerrainShape shape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shape.setLocalScaling(btVector3(128, 128, 1));
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, mOut);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(-215, 215, 1.85963428020477294921875),
osg::Vec3f(-194.9653167724609375, 194.9653167724609375, -6.5760211944580078125),
osg::Vec3f(-174.930633544921875, 174.930633544921875, -15.01167774200439453125),
osg::Vec3f(-154.8959503173828125, 154.8959503173828125, -23.4473323822021484375),
osg::Vec3f(-134.86126708984375, 134.86126708984375, -31.8829898834228515625),
osg::Vec3f(-114.82657623291015625, 114.82657623291015625, -40.3186492919921875),
osg::Vec3f(-94.7918853759765625, 94.7918853759765625, -47.39907073974609375),
osg::Vec3f(-74.75719451904296875, 74.75719451904296875, -53.7258148193359375),
osg::Vec3f(-54.722499847412109375, 54.722499847412109375, -60.052555084228515625),
osg::Vec3f(-34.68780517578125, 34.68780517578125, -66.37929534912109375),
osg::Vec3f(-14.6531162261962890625, 14.6531162261962890625, -72.70604705810546875),
osg::Vec3f(5.3815765380859375, -5.3815765380859375, -75.35065460205078125),
osg::Vec3f(25.41626739501953125, -25.41626739501953125, -67.96945953369140625),
osg::Vec3f(45.450958251953125, -45.450958251953125, -60.58824920654296875),
osg::Vec3f(65.48564910888671875, -65.48564910888671875, -53.20705413818359375),
osg::Vec3f(85.5203399658203125, -85.5203399658203125, -45.825855255126953125),
osg::Vec3f(105.55503082275390625, -105.55503082275390625, -38.44464874267578125),
osg::Vec3f(125.5897216796875, -125.5897216796875, -31.063449859619140625),
osg::Vec3f(145.6244049072265625, -145.6244049072265625, -23.6822509765625),
osg::Vec3f(165.659088134765625, -165.659088134765625, -16.3010540008544921875),
osg::Vec3f(185.6937713623046875, -185.6937713623046875, -8.91985416412353515625),
osg::Vec3f(205.7284698486328125, -205.7284698486328125, -1.53864824771881103515625),
osg::Vec3f(215, -215, 1.877177715301513671875),
})) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, add_object_should_change_navmesh)
{
const std::array<btScalar, 5 * 5> heightfieldData {{
0, 0, 0, 0, 0,
0, -25, -25, -25, -25,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
}};
btHeightfieldTerrainShape heightfieldShape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
heightfieldShape.setLocalScaling(btVector3(128, 128, 1));
btBoxShape boxShape(btVector3(20, 20, 100));
btCompoundShape compoundShape;
compoundShape.addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(0, 0, 0)), &boxShape);
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addObject(ObjectId(&heightfieldShape), heightfieldShape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, std::back_inserter(mPath));
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(-215, 215, 1.85963428020477294921875),
osg::Vec3f(-194.9653167724609375, 194.9653167724609375, -6.5760211944580078125),
osg::Vec3f(-174.930633544921875, 174.930633544921875, -15.01167774200439453125),
osg::Vec3f(-154.8959503173828125, 154.8959503173828125, -23.4473323822021484375),
osg::Vec3f(-134.86126708984375, 134.86126708984375, -31.8829898834228515625),
osg::Vec3f(-114.82657623291015625, 114.82657623291015625, -40.3186492919921875),
osg::Vec3f(-94.7918853759765625, 94.7918853759765625, -47.39907073974609375),
osg::Vec3f(-74.75719451904296875, 74.75719451904296875, -53.7258148193359375),
osg::Vec3f(-54.722499847412109375, 54.722499847412109375, -60.052555084228515625),
osg::Vec3f(-34.68780517578125, 34.68780517578125, -66.37929534912109375),
osg::Vec3f(-14.6531162261962890625, 14.6531162261962890625, -72.70604705810546875),
osg::Vec3f(5.3815765380859375, -5.3815765380859375, -75.35065460205078125),
osg::Vec3f(25.41626739501953125, -25.41626739501953125, -67.96945953369140625),
osg::Vec3f(45.450958251953125, -45.450958251953125, -60.58824920654296875),
osg::Vec3f(65.48564910888671875, -65.48564910888671875, -53.20705413818359375),
osg::Vec3f(85.5203399658203125, -85.5203399658203125, -45.825855255126953125),
osg::Vec3f(105.55503082275390625, -105.55503082275390625, -38.44464874267578125),
osg::Vec3f(125.5897216796875, -125.5897216796875, -31.063449859619140625),
osg::Vec3f(145.6244049072265625, -145.6244049072265625, -23.6822509765625),
osg::Vec3f(165.659088134765625, -165.659088134765625, -16.3010540008544921875),
osg::Vec3f(185.6937713623046875, -185.6937713623046875, -8.91985416412353515625),
osg::Vec3f(205.7284698486328125, -205.7284698486328125, -1.53864824771881103515625),
osg::Vec3f(215, -215, 1.877177715301513671875),
})) << mPath;
mNavigator->addObject(ObjectId(&compoundShape), compoundShape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mPath.clear();
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, std::back_inserter(mPath));
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(-215, 215, 1.87827122211456298828125),
osg::Vec3f(-199.7968292236328125, 191.09100341796875, -3.54876613616943359375),
osg::Vec3f(-184.5936431884765625, 167.1819915771484375, -8.97847270965576171875),
osg::Vec3f(-169.3904571533203125, 143.2729949951171875, -14.40817737579345703125),
osg::Vec3f(-154.1872711181640625, 119.36397552490234375, -19.837890625),
osg::Vec3f(-138.9840850830078125, 95.45496368408203125, -25.2675952911376953125),
osg::Vec3f(-123.78090667724609375, 71.54595184326171875, -30.6972980499267578125),
osg::Vec3f(-108.57772064208984375, 47.636936187744140625, -36.12701416015625),
osg::Vec3f(-93.3745269775390625, 23.7279262542724609375, -40.754688262939453125),
osg::Vec3f(-78.17134857177734375, -0.18108306825160980224609375, -37.128787994384765625),
osg::Vec3f(-62.968158721923828125, -24.0900936126708984375, -33.50289154052734375),
osg::Vec3f(-47.764972686767578125, -47.999103546142578125, -30.797946929931640625),
osg::Vec3f(-23.852447509765625, -63.196765899658203125, -33.97112274169921875),
osg::Vec3f(0.0600789971649646759033203125, -78.39443206787109375, -37.14543914794921875),
osg::Vec3f(23.97260284423828125, -93.5920867919921875, -40.7740936279296875),
osg::Vec3f(47.885128021240234375, -108.78974151611328125, -36.051288604736328125),
osg::Vec3f(71.7976531982421875, -123.98740386962890625, -30.62355804443359375),
osg::Vec3f(95.71018218994140625, -139.18505859375, -25.1958160400390625),
osg::Vec3f(119.6226959228515625, -154.382720947265625, -19.7680912017822265625),
osg::Vec3f(143.53521728515625, -169.58038330078125, -14.3403491973876953125),
osg::Vec3f(167.4477386474609375, -184.778045654296875, -8.91261768341064453125),
osg::Vec3f(191.360260009765625, -199.9757080078125, -3.484879016876220703125),
osg::Vec3f(215, -215, 1.87827455997467041015625),
})) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, update_changed_object_should_change_navmesh)
{
const std::array<btScalar, 5 * 5> heightfieldData {{
0, 0, 0, 0, 0,
0, -25, -25, -25, -25,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
}};
btHeightfieldTerrainShape heightfieldShape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
heightfieldShape.setLocalScaling(btVector3(128, 128, 1));
btBoxShape boxShape(btVector3(20, 20, 100));
btCompoundShape compoundShape;
compoundShape.addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(0, 0, 0)), &boxShape);
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addObject(ObjectId(&heightfieldShape), heightfieldShape, btTransform::getIdentity());
mNavigator->addObject(ObjectId(&compoundShape), compoundShape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, std::back_inserter(mPath));
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(-215, 215, 1.87827122211456298828125),
osg::Vec3f(-199.7968292236328125, 191.09100341796875, -3.54876613616943359375),
osg::Vec3f(-184.5936431884765625, 167.1819915771484375, -8.97847270965576171875),
osg::Vec3f(-169.3904571533203125, 143.2729949951171875, -14.40817737579345703125),
osg::Vec3f(-154.1872711181640625, 119.36397552490234375, -19.837890625),
osg::Vec3f(-138.9840850830078125, 95.45496368408203125, -25.2675952911376953125),
osg::Vec3f(-123.78090667724609375, 71.54595184326171875, -30.6972980499267578125),
osg::Vec3f(-108.57772064208984375, 47.636936187744140625, -36.12701416015625),
osg::Vec3f(-93.3745269775390625, 23.7279262542724609375, -40.754688262939453125),
osg::Vec3f(-78.17134857177734375, -0.18108306825160980224609375, -37.128787994384765625),
osg::Vec3f(-62.968158721923828125, -24.0900936126708984375, -33.50289154052734375),
osg::Vec3f(-47.764972686767578125, -47.999103546142578125, -30.797946929931640625),
osg::Vec3f(-23.852447509765625, -63.196765899658203125, -33.97112274169921875),
osg::Vec3f(0.0600789971649646759033203125, -78.39443206787109375, -37.14543914794921875),
osg::Vec3f(23.97260284423828125, -93.5920867919921875, -40.7740936279296875),
osg::Vec3f(47.885128021240234375, -108.78974151611328125, -36.051288604736328125),
osg::Vec3f(71.7976531982421875, -123.98740386962890625, -30.62355804443359375),
osg::Vec3f(95.71018218994140625, -139.18505859375, -25.1958160400390625),
osg::Vec3f(119.6226959228515625, -154.382720947265625, -19.7680912017822265625),
osg::Vec3f(143.53521728515625, -169.58038330078125, -14.3403491973876953125),
osg::Vec3f(167.4477386474609375, -184.778045654296875, -8.91261768341064453125),
osg::Vec3f(191.360260009765625, -199.9757080078125, -3.484879016876220703125),
osg::Vec3f(215, -215, 1.87827455997467041015625),
})) << mPath;
compoundShape.updateChildTransform(0, btTransform(btMatrix3x3::getIdentity(), btVector3(1000, 0, 0)));
mNavigator->updateObject(ObjectId(&compoundShape), compoundShape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mPath.clear();
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, mOut);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(-215, 215, 1.85963428020477294921875),
osg::Vec3f(-194.9653167724609375, 194.9653167724609375, -6.5760211944580078125),
osg::Vec3f(-174.930633544921875, 174.930633544921875, -15.01167774200439453125),
osg::Vec3f(-154.8959503173828125, 154.8959503173828125, -23.4473323822021484375),
osg::Vec3f(-134.86126708984375, 134.86126708984375, -31.8829898834228515625),
osg::Vec3f(-114.82657623291015625, 114.82657623291015625, -40.3186492919921875),
osg::Vec3f(-94.7918853759765625, 94.7918853759765625, -47.39907073974609375),
osg::Vec3f(-74.75719451904296875, 74.75719451904296875, -53.7258148193359375),
osg::Vec3f(-54.722499847412109375, 54.722499847412109375, -60.052555084228515625),
osg::Vec3f(-34.68780517578125, 34.68780517578125, -66.37929534912109375),
osg::Vec3f(-14.6531162261962890625, 14.6531162261962890625, -72.70604705810546875),
osg::Vec3f(5.3815765380859375, -5.3815765380859375, -75.35065460205078125),
osg::Vec3f(25.41626739501953125, -25.41626739501953125, -67.96945953369140625),
osg::Vec3f(45.450958251953125, -45.450958251953125, -60.58824920654296875),
osg::Vec3f(65.48564910888671875, -65.48564910888671875, -53.20705413818359375),
osg::Vec3f(85.5203399658203125, -85.5203399658203125, -45.825855255126953125),
osg::Vec3f(105.55503082275390625, -105.55503082275390625, -38.44464874267578125),
osg::Vec3f(125.5897216796875, -125.5897216796875, -31.063449859619140625),
osg::Vec3f(145.6244049072265625, -145.6244049072265625, -23.6822509765625),
osg::Vec3f(165.659088134765625, -165.659088134765625, -16.3010540008544921875),
osg::Vec3f(185.6937713623046875, -185.6937713623046875, -8.91985416412353515625),
osg::Vec3f(205.7284698486328125, -205.7284698486328125, -1.53864824771881103515625),
osg::Vec3f(215, -215, 1.877177715301513671875),
})) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, for_overlapping_heightfields_should_use_higher)
{
const std::array<btScalar, 5 * 5> heightfieldData {{
0, 0, 0, 0, 0,
0, -25, -25, -25, -25,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
}};
btHeightfieldTerrainShape shape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shape.setLocalScaling(btVector3(128, 128, 1));
const std::array<btScalar, 5 * 5> heightfieldData2 {{
-25, -25, -25, -25, -25,
-25, -25, -25, -25, -25,
-25, -25, -25, -25, -25,
-25, -25, -25, -25, -25,
-25, -25, -25, -25, -25,
}};
btHeightfieldTerrainShape shape2(5, 5, heightfieldData2.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shape2.setLocalScaling(btVector3(128, 128, 1));
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity());
mNavigator->addObject(ObjectId(&shape2), shape2, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, mOut);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(-215, 215, 1.96328866481781005859375),
osg::Vec3f(-194.9653167724609375, 194.9653167724609375, -0.2422157227993011474609375),
osg::Vec3f(-174.930633544921875, 174.930633544921875, -2.44772052764892578125),
osg::Vec3f(-154.8959503173828125, 154.8959503173828125, -4.653223514556884765625),
osg::Vec3f(-134.86126708984375, 134.86126708984375, -6.858728885650634765625),
osg::Vec3f(-114.82657623291015625, 114.82657623291015625, -9.0642337799072265625),
osg::Vec3f(-94.7918853759765625, 94.7918853759765625, -11.26973724365234375),
osg::Vec3f(-74.75719451904296875, 74.75719451904296875, -13.26497173309326171875),
osg::Vec3f(-54.722499847412109375, 54.722499847412109375, -15.24860286712646484375),
osg::Vec3f(-34.68780517578125, 34.68780517578125, -17.2322368621826171875),
osg::Vec3f(-14.6531162261962890625, 14.6531162261962890625, -19.2158660888671875),
osg::Vec3f(5.3815765380859375, -5.3815765380859375, -20.1338443756103515625),
osg::Vec3f(25.41626739501953125, -25.41626739501953125, -18.150211334228515625),
osg::Vec3f(45.450958251953125, -45.450958251953125, -16.1665802001953125),
osg::Vec3f(65.48564910888671875, -65.48564910888671875, -14.18294811248779296875),
osg::Vec3f(85.5203399658203125, -85.5203399658203125, -12.19931507110595703125),
osg::Vec3f(105.55503082275390625, -105.55503082275390625, -10.08488559722900390625),
osg::Vec3f(125.5897216796875, -125.5897216796875, -7.879383563995361328125),
osg::Vec3f(145.6244049072265625, -145.6244049072265625, -5.673877239227294921875),
osg::Vec3f(165.659088134765625, -165.659088134765625, -3.4683735370635986328125),
osg::Vec3f(185.6937713623046875, -185.6937713623046875, -1.2628715038299560546875),
osg::Vec3f(205.7284698486328125, -205.7284698486328125, 0.9426348209381103515625),
osg::Vec3f(215, -215, 1.96328866481781005859375),
})) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, path_should_be_around_avoid_shape)
{
std::array<btScalar, 5 * 5> heightfieldData {{
0, 0, 0, 0, 0,
0, -25, -25, -25, -25,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
}};
btHeightfieldTerrainShape shape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shape.setLocalScaling(btVector3(128, 128, 1));
std::array<btScalar, 5 * 5> heightfieldDataAvoid {{
-25, -25, -25, -25, -25,
-25, -25, -25, -25, -25,
-25, -25, -25, -25, -25,
-25, -25, -25, -25, -25,
-25, -25, -25, -25, -25,
}};
btHeightfieldTerrainShape shapeAvoid(5, 5, heightfieldDataAvoid.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shapeAvoid.setLocalScaling(btVector3(128, 128, 1));
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addObject(ObjectId(&shape), ObjectShapes {shape, &shapeAvoid}, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, mOut);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(-215, 215, 1.9393787384033203125),
osg::Vec3f(-200.8159637451171875, 190.47265625, -0.639537751674652099609375),
osg::Vec3f(-186.6319427490234375, 165.9453125, -3.2184507846832275390625),
osg::Vec3f(-172.447906494140625, 141.41796875, -5.797363758087158203125),
osg::Vec3f(-158.263885498046875, 116.8906097412109375, -8.37627887725830078125),
osg::Vec3f(-144.079864501953125, 92.3632659912109375, -10.95519161224365234375),
osg::Vec3f(-129.89581298828125, 67.83591461181640625, -13.534107208251953125),
osg::Vec3f(-115.7117919921875, 43.308563232421875, -16.1130199432373046875),
osg::Vec3f(-101.5277557373046875, 18.7812137603759765625, -18.6919345855712890625),
osg::Vec3f(-87.34372711181640625, -5.7461376190185546875, -20.4680538177490234375),
osg::Vec3f(-67.02922821044921875, -25.4970550537109375, -20.514247894287109375),
osg::Vec3f(-46.714717864990234375, -45.2479705810546875, -20.5604457855224609375),
osg::Vec3f(-26.40021514892578125, -64.99889373779296875, -20.6066417694091796875),
osg::Vec3f(-6.085712432861328125, -84.74980926513671875, -20.652835845947265625),
osg::Vec3f(14.22879505157470703125, -104.50072479248046875, -18.151393890380859375),
osg::Vec3f(39.05098724365234375, -118.16222381591796875, -15.6674861907958984375),
osg::Vec3f(63.87317657470703125, -131.82373046875, -13.18357944488525390625),
osg::Vec3f(88.69537353515625, -145.4852142333984375, -10.69967365264892578125),
osg::Vec3f(113.51757049560546875, -159.146697998046875, -8.21576690673828125),
osg::Vec3f(138.3397674560546875, -172.808197021484375, -5.731858730316162109375),
osg::Vec3f(163.1619720458984375, -186.469696044921875, -3.2479503154754638671875),
osg::Vec3f(187.984161376953125, -200.1311798095703125, -0.764044582843780517578125),
osg::Vec3f(212.8063507080078125, -213.7926788330078125, 1.7198636531829833984375),
osg::Vec3f(215, -215, 1.93937528133392333984375),
})) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, path_should_be_over_water_ground_lower_than_water_with_only_swim_flag)
{
std::array<btScalar, 5 * 5> heightfieldData {{
-50, -50, -50, -50, 0,
-50, -100, -150, -100, -50,
-50, -150, -200, -150, -100,
-50, -100, -150, -100, -100,
0, -50, -100, -100, -100,
}};
btHeightfieldTerrainShape shape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shape.setLocalScaling(btVector3(128, 128, 1));
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addWater(osg::Vec2i(0, 0), 128 * 4, 300, btTransform::getIdentity());
mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mStart.x() = 0;
mStart.z() = 300;
mEnd.x() = 0;
mEnd.z() = 300;
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_swim, mOut);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(0, 215, 185.33331298828125),
osg::Vec3f(0, 186.6666717529296875, 185.33331298828125),
osg::Vec3f(0, 158.333343505859375, 185.33331298828125),
osg::Vec3f(0, 130.0000152587890625, 185.33331298828125),
osg::Vec3f(0, 101.66667938232421875, 185.33331298828125),
osg::Vec3f(0, 73.333343505859375, 185.33331298828125),
osg::Vec3f(0, 45.0000152587890625, 185.33331298828125),
osg::Vec3f(0, 16.6666812896728515625, 185.33331298828125),
osg::Vec3f(0, -11.66664981842041015625, 185.33331298828125),
osg::Vec3f(0, -39.999980926513671875, 185.33331298828125),
osg::Vec3f(0, -68.33331298828125, 185.33331298828125),
osg::Vec3f(0, -96.66664886474609375, 185.33331298828125),
osg::Vec3f(0, -124.99997711181640625, 185.33331298828125),
osg::Vec3f(0, -153.33331298828125, 185.33331298828125),
osg::Vec3f(0, -181.6666412353515625, 185.33331298828125),
osg::Vec3f(0, -209.999969482421875, 185.33331298828125),
osg::Vec3f(0, -215, 185.33331298828125),
})) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, path_should_be_over_water_when_ground_cross_water_with_swim_and_walk_flags)
{
std::array<btScalar, 7 * 7> heightfieldData {{
0, 0, 0, 0, 0, 0, 0,
0, -100, -100, -100, -100, -100, 0,
0, -100, -150, -150, -150, -100, 0,
0, -100, -150, -200, -150, -100, 0,
0, -100, -150, -150, -150, -100, 0,
0, -100, -100, -100, -100, -100, 0,
0, 0, 0, 0, 0, 0, 0,
}};
btHeightfieldTerrainShape shape(7, 7, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shape.setLocalScaling(btVector3(128, 128, 1));
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addWater(osg::Vec2i(0, 0), 128 * 4, -25, btTransform::getIdentity());
mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mStart.x() = 0;
mEnd.x() = 0;
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_swim | Flag_walk, mOut);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(0, 215, -94.75363922119140625),
osg::Vec3f(0, 186.6666717529296875, -106.0000152587890625),
osg::Vec3f(0, 158.333343505859375, -115.85507965087890625),
osg::Vec3f(0, 130.0000152587890625, -125.71016693115234375),
osg::Vec3f(0, 101.66667938232421875, -135.5652313232421875),
osg::Vec3f(0, 73.333343505859375, -143.3333587646484375),
osg::Vec3f(0, 45.0000152587890625, -143.3333587646484375),
osg::Vec3f(0, 16.6666812896728515625, -143.3333587646484375),
osg::Vec3f(0, -11.66664981842041015625, -143.3333587646484375),
osg::Vec3f(0, -39.999980926513671875, -143.3333587646484375),
osg::Vec3f(0, -68.33331298828125, -143.3333587646484375),
osg::Vec3f(0, -96.66664886474609375, -137.3043670654296875),
osg::Vec3f(0, -124.99997711181640625, -127.44930267333984375),
osg::Vec3f(0, -153.33331298828125, -117.5942230224609375),
osg::Vec3f(0, -181.6666412353515625, -107.7391510009765625),
osg::Vec3f(0, -209.999969482421875, -97.79712677001953125),
osg::Vec3f(0, -215, -94.753631591796875),
})) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, path_should_be_over_water_when_ground_cross_water_with_max_int_cells_size_and_swim_and_walk_flags)
{
std::array<btScalar, 7 * 7> heightfieldData {{
0, 0, 0, 0, 0, 0, 0,
0, -100, -100, -100, -100, -100, 0,
0, -100, -150, -150, -150, -100, 0,
0, -100, -150, -200, -150, -100, 0,
0, -100, -150, -150, -150, -100, 0,
0, -100, -100, -100, -100, -100, 0,
0, 0, 0, 0, 0, 0, 0,
}};
btHeightfieldTerrainShape shape(7, 7, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shape.setLocalScaling(btVector3(128, 128, 1));
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity());
mNavigator->addWater(osg::Vec2i(0, 0), std::numeric_limits<int>::max(), -25, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mStart.x() = 0;
mEnd.x() = 0;
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_swim | Flag_walk, mOut);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(0, 215, -94.75363922119140625),
osg::Vec3f(0, 186.6666717529296875, -106.0000152587890625),
osg::Vec3f(0, 158.333343505859375, -115.85507965087890625),
osg::Vec3f(0, 130.0000152587890625, -125.71016693115234375),
osg::Vec3f(0, 101.66667938232421875, -135.5652313232421875),
osg::Vec3f(0, 73.333343505859375, -143.3333587646484375),
osg::Vec3f(0, 45.0000152587890625, -143.3333587646484375),
osg::Vec3f(0, 16.6666812896728515625, -143.3333587646484375),
osg::Vec3f(0, -11.66664981842041015625, -143.3333587646484375),
osg::Vec3f(0, -39.999980926513671875, -143.3333587646484375),
osg::Vec3f(0, -68.33331298828125, -143.3333587646484375),
osg::Vec3f(0, -96.66664886474609375, -137.3043670654296875),
osg::Vec3f(0, -124.99997711181640625, -127.44930267333984375),
osg::Vec3f(0, -153.33331298828125, -117.5942230224609375),
osg::Vec3f(0, -181.6666412353515625, -107.7391510009765625),
osg::Vec3f(0, -209.999969482421875, -97.79712677001953125),
osg::Vec3f(0, -215, -94.753631591796875),
})) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, path_should_be_over_ground_when_ground_cross_water_with_only_walk_flag)
{
std::array<btScalar, 7 * 7> heightfieldData {{
0, 0, 0, 0, 0, 0, 0,
0, -100, -100, -100, -100, -100, 0,
0, -100, -150, -150, -150, -100, 0,
0, -100, -150, -200, -150, -100, 0,
0, -100, -150, -150, -150, -100, 0,
0, -100, -100, -100, -100, -100, 0,
0, 0, 0, 0, 0, 0, 0,
}};
btHeightfieldTerrainShape shape(7, 7, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shape.setLocalScaling(btVector3(128, 128, 1));
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addWater(osg::Vec2i(0, 0), 128 * 4, -25, btTransform::getIdentity());
mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mStart.x() = 0;
mEnd.x() = 0;
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, mOut);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(0, 215, -94.75363922119140625),
osg::Vec3f(9.8083515167236328125, 188.4185333251953125, -105.19994354248046875),
osg::Vec3f(19.6167049407958984375, 161.837066650390625, -114.25496673583984375),
osg::Vec3f(29.42505645751953125, 135.255615234375, -123.309967041015625),
osg::Vec3f(39.23340606689453125, 108.674163818359375, -132.3649749755859375),
osg::Vec3f(49.04175567626953125, 82.09270477294921875, -137.2874755859375),
osg::Vec3f(58.8501129150390625, 55.5112457275390625, -139.2451171875),
osg::Vec3f(68.6584625244140625, 28.9297885894775390625, -141.2027740478515625),
osg::Vec3f(78.4668121337890625, 2.3483295440673828125, -143.1604156494140625),
osg::Vec3f(88.27516937255859375, -24.233127593994140625, -141.3894805908203125),
osg::Vec3f(83.73651885986328125, -52.2005767822265625, -142.3761444091796875),
osg::Vec3f(79.19786834716796875, -80.16802978515625, -143.114837646484375),
osg::Vec3f(64.8477935791015625, -104.598602294921875, -137.840911865234375),
osg::Vec3f(50.497714996337890625, -129.0291748046875, -131.45831298828125),
osg::Vec3f(36.147632598876953125, -153.459747314453125, -121.42321014404296875),
osg::Vec3f(21.7975559234619140625, -177.8903350830078125, -111.38809967041015625),
osg::Vec3f(7.44747829437255859375, -202.3209075927734375, -101.1938323974609375),
osg::Vec3f(0, -215, -94.753631591796875),
})) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, update_remove_and_update_then_find_path_should_return_path)
{
const std::array<btScalar, 5 * 5> heightfieldData {{
0, 0, 0, 0, 0,
0, -25, -25, -25, -25,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
}};
btHeightfieldTerrainShape shape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shape.setLocalScaling(btVector3(128, 128, 1));
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mNavigator->removeObject(ObjectId(&shape));
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, mOut);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(-215, 215, 1.85963428020477294921875),
osg::Vec3f(-194.9653167724609375, 194.9653167724609375, -6.5760211944580078125),
osg::Vec3f(-174.930633544921875, 174.930633544921875, -15.01167774200439453125),
osg::Vec3f(-154.8959503173828125, 154.8959503173828125, -23.4473323822021484375),
osg::Vec3f(-134.86126708984375, 134.86126708984375, -31.8829898834228515625),
osg::Vec3f(-114.82657623291015625, 114.82657623291015625, -40.3186492919921875),
osg::Vec3f(-94.7918853759765625, 94.7918853759765625, -47.39907073974609375),
osg::Vec3f(-74.75719451904296875, 74.75719451904296875, -53.7258148193359375),
osg::Vec3f(-54.722499847412109375, 54.722499847412109375, -60.052555084228515625),
osg::Vec3f(-34.68780517578125, 34.68780517578125, -66.37929534912109375),
osg::Vec3f(-14.6531162261962890625, 14.6531162261962890625, -72.70604705810546875),
osg::Vec3f(5.3815765380859375, -5.3815765380859375, -75.35065460205078125),
osg::Vec3f(25.41626739501953125, -25.41626739501953125, -67.96945953369140625),
osg::Vec3f(45.450958251953125, -45.450958251953125, -60.58824920654296875),
osg::Vec3f(65.48564910888671875, -65.48564910888671875, -53.20705413818359375),
osg::Vec3f(85.5203399658203125, -85.5203399658203125, -45.825855255126953125),
osg::Vec3f(105.55503082275390625, -105.55503082275390625, -38.44464874267578125),
osg::Vec3f(125.5897216796875, -125.5897216796875, -31.063449859619140625),
osg::Vec3f(145.6244049072265625, -145.6244049072265625, -23.6822509765625),
osg::Vec3f(165.659088134765625, -165.659088134765625, -16.3010540008544921875),
osg::Vec3f(185.6937713623046875, -185.6937713623046875, -8.91985416412353515625),
osg::Vec3f(205.7284698486328125, -205.7284698486328125, -1.53864824771881103515625),
osg::Vec3f(215, -215, 1.877177715301513671875),
})) << mPath;
}
}

@ -0,0 +1,312 @@
#include "operators.hpp"
#include <components/detournavigator/navmeshtilescache.hpp>
#include <components/detournavigator/exceptions.hpp>
#include <components/detournavigator/recastmesh.hpp>
#include <LinearMath/btTransform.h>
#include <gtest/gtest.h>
namespace DetourNavigator
{
static inline bool operator ==(const NavMeshDataRef& lhs, const NavMeshDataRef& rhs)
{
return std::make_pair(lhs.mValue, lhs.mSize) == std::make_pair(rhs.mValue, rhs.mSize);
}
}
namespace
{
using namespace testing;
using namespace DetourNavigator;
struct DetourNavigatorNavMeshTilesCacheTest : Test
{
const osg::Vec3f mAgentHalfExtents {1, 2, 3};
const TilePosition mTilePosition {0, 0};
const std::vector<int> mIndices {{0, 1, 2}};
const std::vector<float> mVertices {{0, 0, 0, 1, 0, 0, 1, 1, 0}};
const std::vector<AreaType> mAreaTypes {1, AreaType_ground};
const std::vector<RecastMesh::Water> mWater {};
const std::size_t mTrianglesPerChunk {1};
const RecastMesh mRecastMesh {mIndices, mVertices, mAreaTypes, mWater, mTrianglesPerChunk};
const std::vector<OffMeshConnection> mOffMeshConnections {};
unsigned char* const mData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData mNavMeshData {mData, 1};
};
TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_for_empty_cache_should_return_empty_value)
{
const std::size_t maxSize = 0;
NavMeshTilesCache cache(maxSize);
EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_for_not_enought_cache_size_should_return_empty_value)
{
const std::size_t maxSize = 0;
NavMeshTilesCache cache(maxSize);
EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections,
std::move(mNavMeshData)));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_return_cached_value)
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
const auto result = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections,
std::move(mNavMeshData));
EXPECT_EQ(result.get(), (NavMeshDataRef {mData, 1}));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_existing_element_should_throw_exception)
{
const std::size_t maxSize = 2;
NavMeshTilesCache cache(maxSize);
const auto anotherData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData anotherNavMeshData {anotherData, 1};
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData));
EXPECT_THROW(
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(anotherNavMeshData)),
InvalidArgument
);
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_should_return_cached_value)
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData));
const auto result = cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections);
ASSERT_TRUE(result);
EXPECT_EQ(result.get(), (NavMeshDataRef {mData, 1}));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_for_cache_miss_by_agent_half_extents_should_return_empty_value)
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
const osg::Vec3f unexsistentAgentHalfExtents {1, 1, 1};
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData));
EXPECT_FALSE(cache.get(unexsistentAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_for_cache_miss_by_tile_position_should_return_empty_value)
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
const TilePosition unexistentTilePosition {1, 1};
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData));
EXPECT_FALSE(cache.get(mAgentHalfExtents, unexistentTilePosition, mRecastMesh, mOffMeshConnections));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_for_cache_miss_by_recast_mesh_should_return_empty_value)
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
const std::vector<RecastMesh::Water> water {1, RecastMesh::Water {1, btTransform::getIdentity()}};
const RecastMesh unexistentRecastMesh {mIndices, mVertices, mAreaTypes, water, mTrianglesPerChunk};
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData));
EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, unexistentRecastMesh, mOffMeshConnections));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_replace_unused_value)
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
const std::vector<RecastMesh::Water> water {1, RecastMesh::Water {1, btTransform::getIdentity()}};
const RecastMesh anotherRecastMesh {mIndices, mVertices, mAreaTypes, water, mTrianglesPerChunk};
const auto anotherData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData anotherNavMeshData {anotherData, 1};
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData));
const auto result = cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections,
std::move(anotherNavMeshData));
ASSERT_TRUE(result);
EXPECT_EQ(result.get(), (NavMeshDataRef {anotherData, 1}));
EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_not_replace_used_value)
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
const std::vector<RecastMesh::Water> water {1, RecastMesh::Water {1, btTransform::getIdentity()}};
const RecastMesh anotherRecastMesh {mIndices, mVertices, mAreaTypes, water, mTrianglesPerChunk};
const auto anotherData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData anotherNavMeshData {anotherData, 1};
const auto value = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections,
std::move(mNavMeshData));
EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections,
std::move(anotherNavMeshData)));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_replace_unused_least_recently_set_value)
{
const std::size_t maxSize = 2;
NavMeshTilesCache cache(maxSize);
const std::vector<RecastMesh::Water> leastRecentlySetWater {1, RecastMesh::Water {1, btTransform::getIdentity()}};
const RecastMesh leastRecentlySetRecastMesh {mIndices, mVertices, mAreaTypes, leastRecentlySetWater,
mTrianglesPerChunk};
const auto leastRecentlySetData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData leastRecentlySetNavMeshData {leastRecentlySetData, 1};
const std::vector<RecastMesh::Water> mostRecentlySetWater {1, RecastMesh::Water {2, btTransform::getIdentity()}};
const RecastMesh mostRecentlySetRecastMesh {mIndices, mVertices, mAreaTypes, mostRecentlySetWater,
mTrianglesPerChunk};
const auto mostRecentlySetData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData mostRecentlySetNavMeshData {mostRecentlySetData, 1};
ASSERT_TRUE(cache.set(mAgentHalfExtents, mTilePosition, leastRecentlySetRecastMesh, mOffMeshConnections,
std::move(leastRecentlySetNavMeshData)));
ASSERT_TRUE(cache.set(mAgentHalfExtents, mTilePosition, mostRecentlySetRecastMesh, mOffMeshConnections,
std::move(mostRecentlySetNavMeshData)));
const auto result = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections,
std::move(mNavMeshData));
EXPECT_EQ(result.get(), (NavMeshDataRef {mData, 1}));
EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, leastRecentlySetRecastMesh, mOffMeshConnections));
EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mostRecentlySetRecastMesh, mOffMeshConnections));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_replace_unused_least_recently_used_value)
{
const std::size_t maxSize = 2;
NavMeshTilesCache cache(maxSize);
const std::vector<RecastMesh::Water> leastRecentlyUsedWater {1, RecastMesh::Water {1, btTransform::getIdentity()}};
const RecastMesh leastRecentlyUsedRecastMesh {mIndices, mVertices, mAreaTypes, leastRecentlyUsedWater,
mTrianglesPerChunk};
const auto leastRecentlyUsedData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData leastRecentlyUsedNavMeshData {leastRecentlyUsedData, 1};
const std::vector<RecastMesh::Water> mostRecentlyUsedWater {1, RecastMesh::Water {2, btTransform::getIdentity()}};
const RecastMesh mostRecentlyUsedRecastMesh {mIndices, mVertices, mAreaTypes, mostRecentlyUsedWater,
mTrianglesPerChunk};
const auto mostRecentlyUsedData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData mostRecentlyUsedNavMeshData {mostRecentlyUsedData, 1};
cache.set(mAgentHalfExtents, mTilePosition, leastRecentlyUsedRecastMesh, mOffMeshConnections,
std::move(leastRecentlyUsedNavMeshData));
cache.set(mAgentHalfExtents, mTilePosition, mostRecentlyUsedRecastMesh, mOffMeshConnections,
std::move(mostRecentlyUsedNavMeshData));
{
const auto value = cache.get(mAgentHalfExtents, mTilePosition, leastRecentlyUsedRecastMesh, mOffMeshConnections);
ASSERT_TRUE(value);
ASSERT_EQ(value.get(), (NavMeshDataRef {leastRecentlyUsedData, 1}));
}
{
const auto value = cache.get(mAgentHalfExtents, mTilePosition, mostRecentlyUsedRecastMesh, mOffMeshConnections);
ASSERT_TRUE(value);
ASSERT_EQ(value.get(), (NavMeshDataRef {mostRecentlyUsedData, 1}));
}
const auto result = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections,
std::move(mNavMeshData));
EXPECT_EQ(result.get(), (NavMeshDataRef {mData, 1}));
EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, leastRecentlyUsedRecastMesh, mOffMeshConnections));
EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mostRecentlyUsedRecastMesh, mOffMeshConnections));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_not_replace_unused_least_recently_used_value_when_item_does_not_not_fit_cache_max_size)
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
const std::vector<RecastMesh::Water> water {1, RecastMesh::Water {1, btTransform::getIdentity()}};
const RecastMesh tooLargeRecastMesh {mIndices, mVertices, mAreaTypes, water, mTrianglesPerChunk};
const auto tooLargeData = reinterpret_cast<unsigned char*>(dtAlloc(2, DT_ALLOC_PERM));
NavMeshData tooLargeNavMeshData {tooLargeData, 2};
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData));
EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, tooLargeRecastMesh, mOffMeshConnections,
std::move(tooLargeNavMeshData)));
EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_not_replace_unused_least_recently_used_value_when_item_does_not_not_fit_size_of_unused_items)
{
const std::size_t maxSize = 2;
NavMeshTilesCache cache(maxSize);
const std::vector<RecastMesh::Water> anotherWater {1, RecastMesh::Water {1, btTransform::getIdentity()}};
const RecastMesh anotherRecastMesh {mIndices, mVertices, mAreaTypes, anotherWater, mTrianglesPerChunk};
const auto anotherData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData anotherNavMeshData {anotherData, 1};
const std::vector<RecastMesh::Water> tooLargeWater {1, RecastMesh::Water {2, btTransform::getIdentity()}};
const RecastMesh tooLargeRecastMesh {mIndices, mVertices, mAreaTypes, tooLargeWater, mTrianglesPerChunk};
const auto tooLargeData = reinterpret_cast<unsigned char*>(dtAlloc(2, DT_ALLOC_PERM));
NavMeshData tooLargeNavMeshData {tooLargeData, 2};
const auto value = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections,
std::move(mNavMeshData));
ASSERT_TRUE(value);
ASSERT_TRUE(cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections,
std::move(anotherNavMeshData)));
EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, tooLargeRecastMesh, mOffMeshConnections,
std::move(tooLargeNavMeshData)));
EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections));
EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, release_used_after_set_then_used_by_get_item_should_left_this_item_available)
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
const std::vector<RecastMesh::Water> water {1, RecastMesh::Water {1, btTransform::getIdentity()}};
const RecastMesh anotherRecastMesh {mIndices, mVertices, mAreaTypes, water, mTrianglesPerChunk};
const auto anotherData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData anotherNavMeshData {anotherData, 1};
const auto firstCopy = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData));
ASSERT_TRUE(firstCopy);
{
const auto secondCopy = cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections);
ASSERT_TRUE(secondCopy);
}
EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections,
std::move(anotherNavMeshData)));
EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, release_twice_used_item_should_left_this_item_available)
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
const std::vector<RecastMesh::Water> water {1, RecastMesh::Water {1, btTransform::getIdentity()}};
const RecastMesh anotherRecastMesh {mIndices, mVertices, mAreaTypes, water, mTrianglesPerChunk};
const auto anotherData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData anotherNavMeshData {anotherData, 1};
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData));
const auto firstCopy = cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections);
ASSERT_TRUE(firstCopy);
{
const auto secondCopy = cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections);
ASSERT_TRUE(secondCopy);
}
EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections,
std::move(anotherNavMeshData)));
EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections));
}
}

@ -0,0 +1,40 @@
#ifndef OPENMW_TEST_SUITE_DETOURNAVIGATOR_OPERATORS_H
#define OPENMW_TEST_SUITE_DETOURNAVIGATOR_OPERATORS_H
#include <components/bullethelpers/operators.hpp>
#include <components/detournavigator/debug.hpp>
#include <components/osghelpers/operators.hpp>
#include <deque>
#include <iomanip>
#include <iostream>
#include <limits>
#include <sstream>
#include <gtest/gtest.h>
namespace DetourNavigator
{
static inline bool operator ==(const TileBounds& lhs, const TileBounds& rhs)
{
return lhs.mMin == rhs.mMin && lhs.mMax == rhs.mMax;
}
}
namespace testing
{
template <>
inline testing::Message& Message::operator <<(const std::deque<osg::Vec3f>& value)
{
(*this) << "{\n";
for (const auto& v : value)
{
std::ostringstream stream;
stream << v;
(*this) << stream.str() << ",\n";
}
return (*this) << "}";
}
}
#endif

@ -0,0 +1,413 @@
#include "operators.hpp"
#include <components/detournavigator/recastmeshbuilder.hpp>
#include <components/detournavigator/settings.hpp>
#include <components/detournavigator/recastmesh.hpp>
#include <components/detournavigator/exceptions.hpp>
#include <BulletCollision/CollisionShapes/btBoxShape.h>
#include <BulletCollision/CollisionShapes/btBvhTriangleMeshShape.h>
#include <BulletCollision/CollisionShapes/btTriangleMesh.h>
#include <BulletCollision/CollisionShapes/btHeightfieldTerrainShape.h>
#include <BulletCollision/CollisionShapes/btCompoundShape.h>
#include <gtest/gtest.h>
namespace DetourNavigator
{
static inline bool operator ==(const RecastMesh::Water& lhs, const RecastMesh::Water& rhs)
{
return lhs.mCellSize == rhs.mCellSize && lhs.mTransform == rhs.mTransform;
}
}
namespace
{
using namespace testing;
using namespace DetourNavigator;
struct DetourNavigatorRecastMeshBuilderTest : Test
{
Settings mSettings;
TileBounds mBounds;
DetourNavigatorRecastMeshBuilderTest()
{
mSettings.mRecastScaleFactor = 1.0f;
mSettings.mTrianglesPerChunk = 256;
mBounds.mMin = osg::Vec2f(-std::numeric_limits<float>::max() * std::numeric_limits<float>::epsilon(),
-std::numeric_limits<float>::max() * std::numeric_limits<float>::epsilon());
mBounds.mMax = osg::Vec2f(std::numeric_limits<float>::max() * std::numeric_limits<float>::epsilon(),
std::numeric_limits<float>::max() * std::numeric_limits<float>::epsilon());
}
};
TEST_F(DetourNavigatorRecastMeshBuilderTest, create_for_empty_should_return_empty)
{
RecastMeshBuilder builder(mSettings, mBounds);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>());
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>());
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>());
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, add_bhv_triangle_mesh_shape)
{
btTriangleMesh mesh;
mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0));
btBvhTriangleMeshShape shape(&mesh, true);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(static_cast<const btCollisionShape&>(shape), btTransform::getIdentity(), AreaType_ground);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
1, 0, -1,
-1, 0, 1,
-1, 0, -1,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>({AreaType_ground}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, add_transformed_bhv_triangle_mesh_shape)
{
btTriangleMesh mesh;
mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0));
btBvhTriangleMeshShape shape(&mesh, true);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)),
AreaType_ground
);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
2, 3, 0,
0, 3, 4,
0, 3, 0,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>({AreaType_ground}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, add_heightfield_terrian_shape)
{
const std::array<btScalar, 4> heightfieldData {{0, 0, 0, 0}};
btHeightfieldTerrainShape shape(2, 2, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(static_cast<const btCollisionShape&>(shape), btTransform::getIdentity(), AreaType_ground);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
-0.5, 0, -0.5,
-0.5, 0, 0.5,
0.5, 0, -0.5,
0.5, 0, -0.5,
-0.5, 0, 0.5,
0.5, 0, 0.5,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2, 3, 4, 5}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>({AreaType_ground, AreaType_ground}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, add_box_shape_should_produce_12_triangles)
{
btBoxShape shape(btVector3(1, 1, 2));
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(static_cast<const btCollisionShape&>(shape), btTransform::getIdentity(), AreaType_ground);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
1, 2, 1,
-1, 2, 1,
1, 2, -1,
-1, 2, -1,
1, -2, 1,
-1, -2, 1,
1, -2, -1,
-1, -2, -1,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({
0, 2, 3,
3, 1, 0,
0, 4, 6,
6, 2, 0,
0, 1, 5,
5, 4, 0,
7, 5, 1,
1, 3, 7,
7, 3, 2,
2, 6, 7,
7, 6, 4,
4, 5, 7,
}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>(12, AreaType_ground));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, add_compound_shape)
{
btTriangleMesh mesh1;
mesh1.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0));
btBvhTriangleMeshShape triangle1(&mesh1, true);
btBoxShape box(btVector3(1, 1, 2));
btTriangleMesh mesh2;
mesh2.addTriangle(btVector3(1, 1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0));
btBvhTriangleMeshShape triangle2(&mesh2, true);
btCompoundShape shape;
shape.addChildShape(btTransform::getIdentity(), &triangle1);
shape.addChildShape(btTransform::getIdentity(), &box);
shape.addChildShape(btTransform::getIdentity(), &triangle2);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform::getIdentity(),
AreaType_ground
);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
1, 0, -1,
-1, 0, 1,
-1, 0, -1,
1, 2, 1,
-1, 2, 1,
1, 2, -1,
-1, 2, -1,
1, -2, 1,
-1, -2, 1,
1, -2, -1,
-1, -2, -1,
1, 0, -1,
-1, 0, 1,
1, 0, 1,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({
0, 1, 2,
3, 5, 6,
6, 4, 3,
3, 7, 9,
9, 5, 3,
3, 4, 8,
8, 7, 3,
10, 8, 4,
4, 6, 10,
10, 6, 5,
5, 9, 10,
10, 9, 7,
7, 8, 10,
11, 12, 13,
}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>(14, AreaType_ground));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, add_transformed_compound_shape)
{
btTriangleMesh mesh;
mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0));
btBvhTriangleMeshShape triangle(&mesh, true);
btCompoundShape shape;
shape.addChildShape(btTransform::getIdentity(), &triangle);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)),
AreaType_ground
);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
2, 3, 0,
0, 3, 4,
0, 3, 0,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>({AreaType_ground}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, add_transformed_compound_shape_with_transformed_bhv_triangle_shape)
{
btTriangleMesh mesh;
mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0));
btBvhTriangleMeshShape triangle(&mesh, true);
btCompoundShape shape;
shape.addChildShape(btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)),
&triangle);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)),
AreaType_ground
);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
3, 12, 2,
1, 12, 10,
1, 12, 2,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>({AreaType_ground}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, without_bounds_add_bhv_triangle_shape_should_not_filter_by_bounds)
{
btTriangleMesh mesh;
mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0));
mesh.addTriangle(btVector3(-3, -3, 0), btVector3(-3, -2, 0), btVector3(-2, -3, 0));
btBvhTriangleMeshShape shape(&mesh, true);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform::getIdentity(),
AreaType_ground
);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
1, 0, -1,
-1, 0, 1,
-1, 0, -1,
-2, 0, -3,
-3, 0, -2,
-3, 0, -3,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2, 3, 4, 5}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>(2, AreaType_ground));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, with_bounds_add_bhv_triangle_shape_should_filter_by_bounds)
{
mSettings.mRecastScaleFactor = 0.1f;
mBounds.mMin = osg::Vec2f(-3, -3) * mSettings.mRecastScaleFactor;
mBounds.mMax = osg::Vec2f(-2, -2) * mSettings.mRecastScaleFactor;
btTriangleMesh mesh;
mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0));
mesh.addTriangle(btVector3(-3, -3, 0), btVector3(-3, -2, 0), btVector3(-2, -3, 0));
btBvhTriangleMeshShape shape(&mesh, true);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform::getIdentity(),
AreaType_ground
);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
-0.2f, 0, -0.3f,
-0.3f, 0, -0.2f,
-0.3f, 0, -0.3f,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>({AreaType_ground}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, with_bounds_add_rotated_by_x_bhv_triangle_shape_should_filter_by_bounds)
{
mBounds.mMin = osg::Vec2f(-5, -5);
mBounds.mMax = osg::Vec2f(5, -2);
btTriangleMesh mesh;
mesh.addTriangle(btVector3(0, -1, -1), btVector3(0, -1, -1), btVector3(0, 1, -1));
mesh.addTriangle(btVector3(0, -3, -3), btVector3(0, -3, -2), btVector3(0, -2, -3));
btBvhTriangleMeshShape shape(&mesh, true);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform(btQuaternion(btVector3(1, 0, 0),
static_cast<btScalar>(-osg::PI_4))),
AreaType_ground
);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
0, -0.70710659027099609375, -3.535533905029296875,
0, 0.707107067108154296875, -3.535533905029296875,
0, 2.384185791015625e-07, -4.24264049530029296875,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>({AreaType_ground}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, with_bounds_add_rotated_by_y_bhv_triangle_shape_should_filter_by_bounds)
{
mBounds.mMin = osg::Vec2f(-5, -5);
mBounds.mMax = osg::Vec2f(-3, 5);
btTriangleMesh mesh;
mesh.addTriangle(btVector3(-1, 0, -1), btVector3(-1, 0, 1), btVector3(1, 0, -1));
mesh.addTriangle(btVector3(-3, 0, -3), btVector3(-3, 0, -2), btVector3(-2, 0, -3));
btBvhTriangleMeshShape shape(&mesh, true);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform(btQuaternion(btVector3(0, 1, 0),
static_cast<btScalar>(osg::PI_4))),
AreaType_ground
);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
-3.535533905029296875, -0.70710659027099609375, 0,
-3.535533905029296875, 0.707107067108154296875, 0,
-4.24264049530029296875, 2.384185791015625e-07, 0,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>({AreaType_ground}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, with_bounds_add_rotated_by_z_bhv_triangle_shape_should_filter_by_bounds)
{
mBounds.mMin = osg::Vec2f(-5, -5);
mBounds.mMax = osg::Vec2f(-1, -1);
btTriangleMesh mesh;
mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0));
mesh.addTriangle(btVector3(-3, -3, 0), btVector3(-3, -2, 0), btVector3(-2, -3, 0));
btBvhTriangleMeshShape shape(&mesh, true);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform(btQuaternion(btVector3(0, 0, 1),
static_cast<btScalar>(osg::PI_4))),
AreaType_ground
);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
0.707107067108154296875, 0, -3.535533905029296875,
-0.70710659027099609375, 0, -3.535533905029296875,
2.384185791015625e-07, 0, -4.24264049530029296875,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>({AreaType_ground}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, flags_values_should_be_corresponding_to_added_objects)
{
btTriangleMesh mesh1;
mesh1.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0));
btBvhTriangleMeshShape shape1(&mesh1, true);
btTriangleMesh mesh2;
mesh2.addTriangle(btVector3(-3, -3, 0), btVector3(-3, -2, 0), btVector3(-2, -3, 0));
btBvhTriangleMeshShape shape2(&mesh2, true);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(
static_cast<const btCollisionShape&>(shape1),
btTransform::getIdentity(),
AreaType_ground
);
builder.addObject(
static_cast<const btCollisionShape&>(shape2),
btTransform::getIdentity(),
AreaType_null
);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
1, 0, -1,
-1, 0, 1,
-1, 0, -1,
-2, 0, -3,
-3, 0, -2,
-3, 0, -3,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2, 3, 4, 5}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>({AreaType_ground, AreaType_null}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, add_water_then_get_water_should_return_it)
{
RecastMeshBuilder builder(mSettings, mBounds);
builder.addWater(1000, btTransform(btMatrix3x3::getIdentity(), btVector3(100, 200, 300)));
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getWater(), std::vector<RecastMesh::Water>({
RecastMesh::Water {1000, btTransform(btMatrix3x3::getIdentity(), btVector3(100, 200, 300))}
}));
}
}

@ -0,0 +1,72 @@
#include "operators.hpp"
#include <components/detournavigator/recastmeshobject.hpp>
#include <BulletCollision/CollisionShapes/btBoxShape.h>
#include <BulletCollision/CollisionShapes/btCompoundShape.h>
#include <gtest/gtest.h>
namespace
{
using namespace testing;
using namespace DetourNavigator;
struct DetourNavigatorRecastMeshObjectTest : Test
{
btBoxShape mBoxShape {btVector3(1, 2, 3)};
btCompoundShape mCompoundShape {btVector3(1, 2, 3)};
btTransform mTransform {btQuaternion(btVector3(1, 2, 3), 1), btVector3(1, 2, 3)};
DetourNavigatorRecastMeshObjectTest()
{
mCompoundShape.addChildShape(mTransform, std::addressof(mBoxShape));
}
};
TEST_F(DetourNavigatorRecastMeshObjectTest, constructed_object_should_have_shape_and_transform)
{
const RecastMeshObject object(mBoxShape, mTransform, AreaType_ground);
EXPECT_EQ(std::addressof(object.getShape()), std::addressof(mBoxShape));
EXPECT_EQ(object.getTransform(), mTransform);
}
TEST_F(DetourNavigatorRecastMeshObjectTest, update_with_same_transform_for_not_compound_shape_should_return_false)
{
RecastMeshObject object(mBoxShape, mTransform, AreaType_ground);
EXPECT_FALSE(object.update(mTransform, AreaType_ground));
}
TEST_F(DetourNavigatorRecastMeshObjectTest, update_with_different_transform_should_return_true)
{
RecastMeshObject object(mBoxShape, mTransform, AreaType_ground);
EXPECT_TRUE(object.update(btTransform::getIdentity(), AreaType_ground));
}
TEST_F(DetourNavigatorRecastMeshObjectTest, update_with_different_flags_should_return_true)
{
RecastMeshObject object(mBoxShape, mTransform, AreaType_ground);
EXPECT_TRUE(object.update(mTransform, AreaType_null));
}
TEST_F(DetourNavigatorRecastMeshObjectTest, update_for_compound_shape_with_same_transform_and_not_changed_child_transform_should_return_false)
{
RecastMeshObject object(mCompoundShape, mTransform, AreaType_ground);
EXPECT_FALSE(object.update(mTransform, AreaType_ground));
}
TEST_F(DetourNavigatorRecastMeshObjectTest, update_for_compound_shape_with_same_transform_and_changed_child_transform_should_return_true)
{
RecastMeshObject object(mCompoundShape, mTransform, AreaType_ground);
mCompoundShape.updateChildTransform(0, btTransform::getIdentity());
EXPECT_TRUE(object.update(mTransform, AreaType_ground));
}
TEST_F(DetourNavigatorRecastMeshObjectTest, repeated_update_for_compound_shape_without_changes_should_return_false)
{
RecastMeshObject object(mCompoundShape, mTransform, AreaType_ground);
mCompoundShape.updateChildTransform(0, btTransform::getIdentity());
object.update(mTransform, AreaType_ground);
EXPECT_FALSE(object.update(mTransform, AreaType_ground));
}
}

@ -0,0 +1,68 @@
#include "operators.hpp"
#include <components/detournavigator/settingsutils.hpp>
#include <gtest/gtest.h>
namespace
{
using namespace testing;
using namespace DetourNavigator;
struct DetourNavigatorGetTilePositionTest : Test
{
Settings mSettings;
DetourNavigatorGetTilePositionTest()
{
mSettings.mCellSize = 0.5;
mSettings.mTileSize = 64;
}
};
TEST_F(DetourNavigatorGetTilePositionTest, for_zero_coordinates_should_return_zero_tile_position)
{
EXPECT_EQ(getTilePosition(mSettings, osg::Vec3f(0, 0, 0)), TilePosition(0, 0));
}
TEST_F(DetourNavigatorGetTilePositionTest, tile_size_should_be_multiplied_by_cell_size)
{
EXPECT_EQ(getTilePosition(mSettings, osg::Vec3f(32, 0, 0)), TilePosition(1, 0));
}
TEST_F(DetourNavigatorGetTilePositionTest, tile_position_calculates_by_floor)
{
EXPECT_EQ(getTilePosition(mSettings, osg::Vec3f(31, 0, 0)), TilePosition(0, 0));
}
TEST_F(DetourNavigatorGetTilePositionTest, tile_position_depends_on_x_and_z_coordinates)
{
EXPECT_EQ(getTilePosition(mSettings, osg::Vec3f(32, 64, 128)), TilePosition(1, 4));
}
TEST_F(DetourNavigatorGetTilePositionTest, tile_position_works_for_negative_coordinates)
{
EXPECT_EQ(getTilePosition(mSettings, osg::Vec3f(-31, 0, -32)), TilePosition(-1, -1));
}
struct DetourNavigatorMakeTileBoundsTest : Test
{
Settings mSettings;
DetourNavigatorMakeTileBoundsTest()
{
mSettings.mCellSize = 0.5;
mSettings.mTileSize = 64;
}
};
TEST_F(DetourNavigatorMakeTileBoundsTest, tile_bounds_depend_on_tile_size_and_cell_size)
{
EXPECT_EQ(makeTileBounds(mSettings, TilePosition(0, 0)), (TileBounds {osg::Vec2f(0, 0), osg::Vec2f(32, 32)}));
}
TEST_F(DetourNavigatorMakeTileBoundsTest, tile_bounds_are_multiplied_by_tile_position)
{
EXPECT_EQ(makeTileBounds(mSettings, TilePosition(1, 2)), (TileBounds {osg::Vec2f(32, 64), osg::Vec2f(64, 96)}));
}
}

@ -142,6 +142,7 @@ namespace Resource
static bool operator ==(const Resource::BulletShape& lhs, const Resource::BulletShape& rhs)
{
return compareObjects(lhs.mCollisionShape, rhs.mCollisionShape)
&& compareObjects(lhs.mAvoidCollisionShape, rhs.mAvoidCollisionShape)
&& lhs.mCollisionBoxHalfExtents == rhs.mCollisionBoxHalfExtents
&& lhs.mCollisionBoxTranslate == rhs.mCollisionBoxTranslate
&& lhs.mAnimatedShapes == rhs.mAnimatedShapes;
@ -151,6 +152,7 @@ namespace Resource
{
return stream << "Resource::BulletShape {"
<< value.mCollisionShape << ", "
<< value.mAvoidCollisionShape << ", "
<< value.mCollisionBoxHalfExtents << ", "
<< value.mAnimatedShapes
<< "}";
@ -266,7 +268,8 @@ namespace
value.target = Nif::ControlledPtr(nullptr);
}
void copy(const btTransform& src, Nif::Transformation& dst) {
void copy(const btTransform& src, Nif::Transformation& dst)
{
dst.pos = osg::Vec3f(src.getOrigin().x(), src.getOrigin().y(), src.getOrigin().z());
for (int row = 0; row < 3; ++row)
for (int column = 0; column < 3; ++column)
@ -837,7 +840,10 @@ namespace
EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif"));
const auto result = mLoader.load(mNifFile);
std::unique_ptr<btTriangleMesh> triangles(new btTriangleMesh(false));
triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0));
Resource::BulletShape expected;
expected.mAvoidCollisionShape = new Resource::TriangleMeshShape(triangles.release(), false);
EXPECT_EQ(*result, expected);
}
@ -929,10 +935,8 @@ namespace
mNiStringExtraData.string = "MRK";
mNiStringExtraData.recType = Nif::RC_NiStringExtraData;
mNiTriShape.extra = Nif::ExtraPtr(&mNiStringExtraData);
mNiNode3.children = Nif::NodeList(std::vector<Nif::NodePtr>({Nif::NodePtr(&mNiTriShape)}));
mNiNode3.recType = Nif::RC_RootCollisionNode;
mNiNode2.children = Nif::NodeList(std::vector<Nif::NodePtr>({Nif::NodePtr(nullptr), Nif::NodePtr(&mNiNode3)}));
mNiNode2.recType = Nif::RC_NiNode;
mNiNode2.children = Nif::NodeList(std::vector<Nif::NodePtr>({Nif::NodePtr(&mNiTriShape)}));
mNiNode2.recType = Nif::RC_RootCollisionNode;
mNiNode.children = Nif::NodeList(std::vector<Nif::NodePtr>({Nif::NodePtr(&mNiNode2)}));
mNiNode.recType = Nif::RC_NiNode;

@ -0,0 +1,40 @@
find_path(RecastNavigation_INCLUDE_DIR
NAMES Recast.h
HINTS $ENV{RecastNavigation_ROOT}
${RecastNavigation_ROOT}
PATH_SUFFIXES include
)
mark_as_advanced(RecastNavigation_INCLUDE_DIR)
include(FindPackageHandleStandardArgs)
set(RecastNavigation_LIBRARIES "")
foreach(COMPONENT ${RecastNavigation_FIND_COMPONENTS})
if(NOT RecastNavigation_${COMPONENT}_FOUND)
find_library(RecastNavigation_${COMPONENT}_LIBRARY
HINTS $ENV{RecastNavigation_ROOT}
${RecastNavigation_ROOT}
NAMES ${COMPONENT}
PATH_SUFFIXES lib
)
find_package_handle_standard_args(RecastNavigation_${COMPONENT} DEFAULT_MSG
RecastNavigation_${COMPONENT}_LIBRARY
RecastNavigation_INCLUDE_DIR
)
mark_as_advanced(RecastNavigation_${COMPONENT}_LIBRARY)
if(RecastNavigation_${COMPONENT}_FOUND)
list(APPEND RecastNavigation_LIBRARIES ${RecastNavigation_${COMPONENT}_LIBRARY})
endif()
endif()
endforeach()
mark_as_advanced(RecastNavigation_LIBRARIES)
find_package_handle_standard_args(RecastNavigation DEFAULT_MSG
RecastNavigation_LIBRARIES
RecastNavigation_INCLUDE_DIR
)
if(RecastNavigation_FOUND)
set(RecastNavigation_INCLUDE_DIRS ${RecastNavigation_INCLUDE_DIR})
endif()

@ -51,7 +51,7 @@ add_component_dir (shader
add_component_dir (sceneutil
clone attach visitor util statesetupdater controller skeleton riggeometry morphgeometry lightcontroller
lightmanager lightutil positionattitudetransform workqueue unrefqueue pathgridutil waterutil writescene serialize optimizer
actorutil shadow mwshadowtechnique
actorutil detourdebugdraw navmesh agentpath shadow mwshadowtechnique
)
add_component_dir (nif
@ -156,6 +156,23 @@ if(NOT WIN32 AND NOT ANDROID)
)
endif()
add_component_dir(detournavigator
debug
makenavmesh
findsmoothpath
recastmeshbuilder
recastmeshmanager
cachedrecastmeshmanager
navmeshmanager
navigator
asyncnavmeshupdater
chunkytrimesh
recastmesh
tilecachedrecastmeshmanager
recastmeshobject
navmeshtilescache
)
set (ESM_UI ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui
)
@ -196,6 +213,10 @@ include_directories(${Bullet_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR})
add_library(components STATIC ${COMPONENT_FILES} ${MOC_SRCS} ${ESM_UI_HDR})
find_package(RecastNavigation COMPONENTS DebugUtils Detour Recast REQUIRED)
include_directories(SYSTEM ${RecastNavigation_INCLUDE_DIRS})
target_link_libraries(components
${Boost_SYSTEM_LIBRARY}
${Boost_FILESYSTEM_LIBRARY}
@ -215,6 +236,7 @@ target_link_libraries(components
${SDL2_LIBRARIES}
${OPENGL_gl_LIBRARY}
${MyGUI_LIBRARIES}
${RecastNavigation_LIBRARIES}
)
if (WIN32)

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save