1
0
Fork 1
mirror of https://github.com/TES3MP/openmw-tes3mp.git synced 2025-01-30 16:15:31 +00:00

Add OpenMW commits up to 9 May 2020

# Conflicts:
#	apps/openmw/mwmechanics/spellcasting.cpp
#	apps/openmw/mwscript/aiextensions.cpp
This commit is contained in:
David Cernat 2020-05-09 15:17:04 +03:00
commit 053a5a6258
101 changed files with 2293 additions and 1279 deletions

View file

@ -9,6 +9,7 @@
Bug #5367: Selecting a spell on an enchanted item per hotkey always plays the equip sound Bug #5367: Selecting a spell on an enchanted item per hotkey always plays the equip sound
Bug #5369: Spawnpoint in the Grazelands doesn't produce oversized creatures Bug #5369: Spawnpoint in the Grazelands doesn't produce oversized creatures
Bug #5370: Opening an unlocked but trapped door uses the key Bug #5370: Opening an unlocked but trapped door uses the key
Bug #5400: Editor: Verifier checks race of non-skin bodyparts
Feature #5362: Show the soul gems' trapped soul in count dialog Feature #5362: Show the soul gems' trapped soul in count dialog
0.46.0 0.46.0
@ -217,7 +218,6 @@
Bug #5264: "Damage Fatigue" Magic Effect Can Bring Fatigue below 0 Bug #5264: "Damage Fatigue" Magic Effect Can Bring Fatigue below 0
Bug #5269: Editor: Cell lighting in resaved cleaned content files is corrupted Bug #5269: Editor: Cell lighting in resaved cleaned content files is corrupted
Bug #5278: Console command Show doesn't fall back to global variable after local var not found Bug #5278: Console command Show doesn't fall back to global variable after local var not found
Bug #5300: NPCs don't switch from torch to shield when starting combat
Bug #5308: World map copying makes save loading much slower Bug #5308: World map copying makes save loading much slower
Bug #5313: Node properties of identical type are not applied in the correct order Bug #5313: Node properties of identical type are not applied in the correct order
Bug #5326: Formatting issues in the settings.cfg Bug #5326: Formatting issues in the settings.cfg

View file

@ -759,9 +759,7 @@ echo
cd $DEPS_INSTALL/.. cd $DEPS_INSTALL/..
echo echo
echo "Setting up OpenMW build..." echo "Setting up OpenMW build..."
add_cmake_opts -DBUILD_BSATOOL=no \ add_cmake_opts -DBUILD_MYGUI_PLUGIN=no \
-DBUILD_ESMTOOL=no \
-DBUILD_MYGUI_PLUGIN=no \
-DOPENMW_MP_BUILD=on -DOPENMW_MP_BUILD=on
if [ ! -z $CI ]; then if [ ! -z $CI ]; then
case $STEP in case $STEP in

View file

@ -1,6 +1,6 @@
#!/bin/sh -e #!/bin/sh -e
git clone -b release-1.8.1 https://github.com/google/googletest.git git clone -b release-1.10.0 https://github.com/google/googletest.git
cd googletest cd googletest
mkdir build mkdir build
cd build cd build

View file

@ -33,13 +33,10 @@ void CSMTools::BodyPartCheckStage::perform (int stage, CSMDoc::Messages &message
CSMWorld::UniversalId id( CSMWorld::UniversalId::Type_BodyPart, bodyPart.mId ); CSMWorld::UniversalId id( CSMWorld::UniversalId::Type_BodyPart, bodyPart.mId );
// Check BYDT // Check BYDT
if (bodyPart.mData.mPart > 14 ) if (bodyPart.mData.mPart >= ESM::BodyPart::MP_Count )
messages.add(id, "Invalid part", "", CSMDoc::Message::Severity_Error); messages.add(id, "Invalid part", "", CSMDoc::Message::Severity_Error);
if (bodyPart.mData.mFlags > 3 ) if (bodyPart.mData.mType > ESM::BodyPart::MT_Armor )
messages.add(id, "Invalid flags", "", CSMDoc::Message::Severity_Error);
if (bodyPart.mData.mType > 2 )
messages.add(id, "Invalid type", "", CSMDoc::Message::Severity_Error); messages.add(id, "Invalid type", "", CSMDoc::Message::Severity_Error);
// Check MODL // Check MODL
@ -48,9 +45,12 @@ void CSMTools::BodyPartCheckStage::perform (int stage, CSMDoc::Messages &message
else if ( mMeshes.searchId( bodyPart.mModel ) == -1 ) else if ( mMeshes.searchId( bodyPart.mModel ) == -1 )
messages.add(id, "Model '" + bodyPart.mModel + "' does not exist", "", CSMDoc::Message::Severity_Error); messages.add(id, "Model '" + bodyPart.mModel + "' does not exist", "", CSMDoc::Message::Severity_Error);
// Check FNAM // Check FNAM for skin body parts (for non-skin body parts it's meaningless)
if ( bodyPart.mRace.empty() ) if ( bodyPart.mData.mType == ESM::BodyPart::MT_Skin )
messages.add(id, "Race is missing", "", CSMDoc::Message::Severity_Error); {
else if ( mRaces.searchId( bodyPart.mRace ) == -1 ) if ( bodyPart.mRace.empty() )
messages.add(id, "Race '" + bodyPart.mRace + "' does not exist", "", CSMDoc::Message::Severity_Error); messages.add(id, "Race is missing", "", CSMDoc::Message::Severity_Error);
else if ( mRaces.searchId( bodyPart.mRace ) == -1 )
messages.add(id, "Race '" + bodyPart.mRace + "' does not exist", "", CSMDoc::Message::Severity_Error);
}
} }

View file

@ -83,9 +83,10 @@ add_openmw_dir (mwclass
add_openmw_dir (mwmechanics add_openmw_dir (mwmechanics
mechanicsmanagerimp stat creaturestats magiceffects movement actorutil mechanicsmanagerimp stat creaturestats magiceffects movement actorutil
drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor aibreathe drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor aibreathe
aicast aiescort aiface aiactivate aicombat recharge repair enchanting pathfinding pathgrid security spellsuccess spellcasting aicast aiescort aiface aiactivate aicombat recharge repair enchanting pathfinding pathgrid security spellcasting spellresistance
disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction actor summoning disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction actor summoning
character actors objects aistate coordinateconverter trading weaponpriority spellpriority weapontype character actors objects aistate coordinateconverter trading weaponpriority spellpriority weapontype spellutil tickableeffects
spellabsorption linkedeffects
) )
add_openmw_dir (mwstate add_openmw_dir (mwstate

View file

@ -780,7 +780,6 @@ private:
}; };
// Initialise and enter main loop. // Initialise and enter main loop.
void OMW::Engine::go() void OMW::Engine::go()
{ {
assert (!mContentFiles.empty()); assert (!mContentFiles.empty());
@ -820,7 +819,8 @@ void OMW::Engine::go()
mViewer->setUseConfigureAffinity(false); mViewer->setUseConfigureAffinity(false);
#endif #endif
mScreenCaptureOperation = new WriteScreenshotToFileOperation(mCfgMgr.getUserDataPath().string(), mScreenCaptureOperation = new WriteScreenshotToFileOperation(
mCfgMgr.getScreenshotPath().string(),
Settings::Manager::getString("screenshot format", "General")); Settings::Manager::getString("screenshot format", "General"));
mScreenCaptureHandler = new osgViewer::ScreenCaptureHandler(mScreenCaptureOperation); mScreenCaptureHandler = new osgViewer::ScreenCaptureHandler(mScreenCaptureOperation);

View file

@ -477,6 +477,8 @@ namespace MWGui
void DialogueWindow::onClose() void DialogueWindow::onClose()
{ {
if (MWBase::Environment::get().getWindowManager()->containsMode(GM_Dialogue))
return;
// Reset history // Reset history
for (DialogueText* text : mHistoryContents) for (DialogueText* text : mHistoryContents)
delete text; delete text;

View file

@ -32,26 +32,6 @@ namespace MWGui
{ {
} }
bool ItemStack::stacks(const ItemStack &other)
{
if(mBase == other.mBase)
return true;
// If one of the items is in an inventory and currently equipped, we need to check stacking both ways to be sure
if (mBase.getContainerStore() && other.mBase.getContainerStore())
return mBase.getContainerStore()->stacks(mBase, other.mBase)
&& other.mBase.getContainerStore()->stacks(mBase, other.mBase);
if (mBase.getContainerStore())
return mBase.getContainerStore()->stacks(mBase, other.mBase);
if (other.mBase.getContainerStore())
return other.mBase.getContainerStore()->stacks(mBase, other.mBase);
MWWorld::ContainerStore store;
return store.stacks(mBase, other.mBase);
}
bool operator == (const ItemStack& left, const ItemStack& right) bool operator == (const ItemStack& left, const ItemStack& right)
{ {
if (left.mType != right.mType) if (left.mType != right.mType)

View file

@ -13,7 +13,6 @@ namespace MWGui
{ {
ItemStack (const MWWorld::Ptr& base, ItemModel* creator, size_t count); ItemStack (const MWWorld::Ptr& base, ItemModel* creator, size_t count);
ItemStack(); ItemStack();
bool stacks (const ItemStack& other);
///< like operator==, only without checking mType ///< like operator==, only without checking mType
enum Type enum Type

View file

@ -334,12 +334,6 @@ namespace MWGui
setupCopyFramebufferToTextureCallback(); setupCopyFramebufferToTextureCallback();
} }
// Turn off rendering except the GUI
int oldUpdateMask = mViewer->getUpdateVisitor()->getTraversalMask();
int oldCullMask = mViewer->getCamera()->getCullMask();
mViewer->getUpdateVisitor()->setTraversalMask(MWRender::Mask_GUI|MWRender::Mask_PreCompile);
mViewer->getCamera()->setCullMask(MWRender::Mask_GUI|MWRender::Mask_PreCompile);
MWBase::Environment::get().getInputManager()->update(0, true, true); MWBase::Environment::get().getInputManager()->update(0, true, true);
//osg::Timer timer; //osg::Timer timer;
@ -355,10 +349,6 @@ namespace MWGui
//if (mViewer->getIncrementalCompileOperation()) //if (mViewer->getIncrementalCompileOperation())
//std::cout << "num to compile " << mViewer->getIncrementalCompileOperation()->getToCompile().size() << std::endl; //std::cout << "num to compile " << mViewer->getIncrementalCompileOperation()->getToCompile().size() << std::endl;
// resume 3d rendering
mViewer->getUpdateVisitor()->setTraversalMask(oldUpdateMask);
mViewer->getCamera()->setCullMask(oldCullMask);
mLastRenderTime = mTimer.time_m(); mLastRenderTime = mTimer.time_m();
} }

View file

@ -31,7 +31,7 @@
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
#include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/spellutil.hpp"
#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/creaturestats.hpp"
#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/actorutil.hpp"

View file

@ -27,10 +27,10 @@
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp" #include "../mwworld/esmstore.hpp"
#include "../mwmechanics/spellcasting.hpp"
#include "../mwmechanics/spells.hpp" #include "../mwmechanics/spells.hpp"
#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/creaturestats.hpp"
#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/actorutil.hpp"
#include "../mwmechanics/spellutil.hpp"
#include "tooltips.hpp" #include "tooltips.hpp"
#include "class.hpp" #include "class.hpp"

View file

@ -7,7 +7,7 @@
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/creaturestats.hpp"
#include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/spellutil.hpp"
#include "../mwworld/esmstore.hpp" #include "../mwworld/esmstore.hpp"
#include "../mwworld/inventorystore.hpp" #include "../mwworld/inventorystore.hpp"

View file

@ -29,7 +29,7 @@
#include "../mwworld/esmstore.hpp" #include "../mwworld/esmstore.hpp"
#include "../mwworld/player.hpp" #include "../mwworld/player.hpp"
#include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/spellutil.hpp"
#include "../mwmechanics/spells.hpp" #include "../mwmechanics/spells.hpp"
#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/creaturestats.hpp"
#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/actorutil.hpp"

View file

@ -17,7 +17,7 @@
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp" #include "../mwworld/esmstore.hpp"
#include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/spellutil.hpp"
#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/actorutil.hpp"
#include "mapwindow.hpp" #include "mapwindow.hpp"

View file

@ -142,7 +142,9 @@ namespace MWGui
osgViewer::Viewer* viewer, osg::Group* guiRoot, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, osgViewer::Viewer* viewer, osg::Group* guiRoot, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue,
const std::string& logpath, const std::string& resourcePath, bool consoleOnlyScripts, Translation::Storage& translationDataStorage, const std::string& logpath, const std::string& resourcePath, bool consoleOnlyScripts, Translation::Storage& translationDataStorage,
ToUTF8::FromType encoding, bool exportFonts, const std::string& versionDescription, const std::string& userDataPath) ToUTF8::FromType encoding, bool exportFonts, const std::string& versionDescription, const std::string& userDataPath)
: mStore(nullptr) : mOldUpdateMask(0)
, mOldCullMask(0)
, mStore(nullptr)
, mResourceSystem(resourceSystem) , mResourceSystem(resourceSystem)
, mWorkQueue(workQueue) , mWorkQueue(workQueue)
, mViewer(viewer) , mViewer(viewer)
@ -697,13 +699,34 @@ namespace MWGui
} }
} }
void WindowManager::enableScene(bool enable)
{
unsigned int disablemask = MWRender::Mask_GUI|MWRender::Mask_PreCompile;
if (!enable && mViewer->getCamera()->getCullMask() != disablemask)
{
mOldUpdateMask = mViewer->getUpdateVisitor()->getTraversalMask();
mOldCullMask = mViewer->getCamera()->getCullMask();
mViewer->getUpdateVisitor()->setTraversalMask(disablemask);
mViewer->getCamera()->setCullMask(disablemask);
}
else if (enable && mViewer->getCamera()->getCullMask() == disablemask)
{
mViewer->getUpdateVisitor()->setTraversalMask(mOldUpdateMask);
mViewer->getCamera()->setCullMask(mOldCullMask);
}
}
void WindowManager::updateVisible() void WindowManager::updateVisible()
{ {
bool loading = (getMode() == GM_Loading || getMode() == GM_LoadingWallpaper);
bool mainmenucover = containsMode(GM_MainMenu) && MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame;
enableScene(!loading && !mainmenucover);
if (!mMap) if (!mMap)
return; // UI not created yet return; // UI not created yet
bool loading = (getMode() == GM_Loading || getMode() == GM_LoadingWallpaper);
mHud->setVisible(mHudEnabled && !loading); mHud->setVisible(mHudEnabled && !loading);
mToolTips->setVisible(mHudEnabled && !loading); mToolTips->setVisible(mHudEnabled && !loading);
@ -2005,11 +2028,7 @@ namespace MWGui
mVideoBackground->eventKeyButtonPressed += MyGUI::newDelegate(this, &WindowManager::onVideoKeyPressed); mVideoBackground->eventKeyButtonPressed += MyGUI::newDelegate(this, &WindowManager::onVideoKeyPressed);
} }
// Turn off all rendering except for the GUI enableScene(false);
int oldUpdateMask = mViewer->getUpdateVisitor()->getTraversalMask();
int oldCullMask = mViewer->getCamera()->getCullMask();
mViewer->getUpdateVisitor()->setTraversalMask(MWRender::Mask_GUI);
mViewer->getCamera()->setCullMask(MWRender::Mask_GUI);
MyGUI::IntSize screenSize = MyGUI::RenderManager::getInstance().getViewSize(); MyGUI::IntSize screenSize = MyGUI::RenderManager::getInstance().getViewSize();
sizeVideo(screenSize.width, screenSize.height); sizeVideo(screenSize.width, screenSize.height);
@ -2065,8 +2084,7 @@ namespace MWGui
setCursorVisible(cursorWasVisible); setCursorVisible(cursorWasVisible);
// Restore normal rendering // Restore normal rendering
mViewer->getUpdateVisitor()->setTraversalMask(oldUpdateMask); updateVisible();
mViewer->getCamera()->setCullMask(oldCullMask);
mVideoBackground->setVisible(false); mVideoBackground->setVisible(false);
} }

View file

@ -476,6 +476,8 @@ namespace MWGui
virtual bool injectKeyRelease(MyGUI::KeyCode key); virtual bool injectKeyRelease(MyGUI::KeyCode key);
private: private:
unsigned int mOldUpdateMask; unsigned int mOldCullMask;
const MWWorld::ESMStore* mStore; const MWWorld::ESMStore* mStore;
Resource::ResourceSystem* mResourceSystem; Resource::ResourceSystem* mResourceSystem;
osg::ref_ptr<SceneUtil::WorkQueue> mWorkQueue; osg::ref_ptr<SceneUtil::WorkQueue> mWorkQueue;
@ -657,6 +659,8 @@ namespace MWGui
void setMenuTransparency(float value); void setMenuTransparency(float value);
void updatePinnedWindows(); void updatePinnedWindows();
void enableScene(bool enable);
}; };
} }

View file

@ -59,6 +59,7 @@
#include "summoning.hpp" #include "summoning.hpp"
#include "combat.hpp" #include "combat.hpp"
#include "actorutil.hpp" #include "actorutil.hpp"
#include "tickableeffects.hpp"
namespace namespace
{ {
@ -1310,11 +1311,6 @@ namespace MWMechanics
if (heldIter != inventoryStore.end() && heldIter->getTypeName() != typeid(ESM::Light).name()) if (heldIter != inventoryStore.end() && heldIter->getTypeName() != typeid(ESM::Light).name())
inventoryStore.unequipItem(*heldIter, ptr); inventoryStore.unequipItem(*heldIter, ptr);
} }
else if (heldIter == inventoryStore.end() || heldIter->getTypeName() == typeid(ESM::Light).name())
{
// For hostile NPCs, see if they have anything better to equip first
inventoryStore.autoEquip(ptr);
}
heldIter = inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); heldIter = inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft);

View file

@ -14,7 +14,6 @@
#include "../mwworld/cellstore.hpp" #include "../mwworld/cellstore.hpp"
#include "npcstats.hpp" #include "npcstats.hpp"
#include "spellcasting.hpp"
#include "combat.hpp" #include "combat.hpp"
#include "weaponpriority.hpp" #include "weaponpriority.hpp"
#include "spellpriority.hpp" #include "spellpriority.hpp"

View file

@ -1,5 +1,4 @@
#include "autocalcspell.hpp" #include "autocalcspell.hpp"
#include "spellcasting.hpp"
#include <limits> #include <limits>
@ -8,6 +7,7 @@
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "spellutil.hpp"
namespace MWMechanics namespace MWMechanics
{ {

View file

@ -33,6 +33,7 @@
#include "npcstats.hpp" #include "npcstats.hpp"
#include "movement.hpp" #include "movement.hpp"
#include "spellcasting.hpp" #include "spellcasting.hpp"
#include "spellresistance.hpp"
#include "difficultyscaling.hpp" #include "difficultyscaling.hpp"
#include "actorutil.hpp" #include "actorutil.hpp"
#include "pathfinding.hpp" #include "pathfinding.hpp"

View file

@ -8,7 +8,7 @@
Include additional headers for multiplayer purposes Include additional headers for multiplayer purposes
*/ */
#include <components/openmw-mp/TimedLog.hpp> #include <components/openmw-mp/TimedLog.hpp>
#include "spellcasting.hpp" #include "summoning.hpp"
/* /*
End of tes3mp addition End of tes3mp addition
*/ */

View file

@ -27,7 +27,7 @@
#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/mechanicsmanager.hpp"
#include "creaturestats.hpp" #include "creaturestats.hpp"
#include "spellcasting.hpp" #include "spellutil.hpp"
#include "actorutil.hpp" #include "actorutil.hpp"
#include "weapontype.hpp" #include "weapontype.hpp"

View file

@ -0,0 +1,74 @@
#include "linkedeffects.hpp"
#include <components/misc/rng.hpp>
#include <components/settings/settings.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
#include "../mwrender/animation.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp"
#include "creaturestats.hpp"
namespace MWMechanics
{
bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect,
const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects)
{
if (caster.isEmpty() || caster == target || !target.getClass().isActor())
return false;
bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful;
bool isUnreflectable = magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable;
if (!isHarmful || isUnreflectable)
return false;
float reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).getMagnitude();
if (Misc::Rng::roll0to99() >= reflect)
return false;
const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find ("VFX_Reflect");
MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target);
if (animation && !reflectStatic->mModel.empty())
animation->addEffect("meshes\\" + reflectStatic->mModel, ESM::MagicEffect::Reflect, false, std::string());
reflectedEffects.mList.emplace_back(effect);
return true;
}
void absorbStat(const ESM::ENAMstruct& effect, const ESM::ActiveEffect& appliedEffect,
const MWWorld::Ptr& caster, const MWWorld::Ptr& target, bool reflected, const std::string& source)
{
if (caster.isEmpty() || caster == target)
return;
if (!target.getClass().isActor() || !caster.getClass().isActor())
return;
// Make sure callers don't do something weird
if (effect.mEffectID < ESM::MagicEffect::AbsorbAttribute || effect.mEffectID > ESM::MagicEffect::AbsorbSkill)
throw std::runtime_error("invalid absorb stat effect");
if (appliedEffect.mMagnitude == 0)
return;
std::vector<ActiveSpells::ActiveEffect> absorbEffects;
ActiveSpells::ActiveEffect absorbEffect = appliedEffect;
absorbEffect.mMagnitude *= -1;
absorbEffects.emplace_back(absorbEffect);
// Morrowind negates reflected Absorb spells so the original caster won't be harmed.
if (reflected && Settings::Manager::getBool("classic reflected absorb spells behavior", "Game"))
{
target.getClass().getCreatureStats(target).getActiveSpells().addSpell(std::string(), true,
absorbEffects, source, caster.getClass().getCreatureStats(caster).getActorId());
return;
}
caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(std::string(), true,
absorbEffects, source, target.getClass().getCreatureStats(target).getActorId());
}
}

View file

@ -0,0 +1,32 @@
#ifndef MWMECHANICS_LINKEDEFFECTS_H
#define MWMECHANICS_LINKEDEFFECTS_H
#include <string>
namespace ESM
{
struct ActiveEffect;
struct EffectList;
struct ENAMstruct;
struct MagicEffect;
struct Spell;
}
namespace MWWorld
{
class Ptr;
}
namespace MWMechanics
{
// Try to reflect a spell effect. If it's reflected, it's also put into the passed reflected effects list.
bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect,
const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects);
// Try to absorb a stat (skill, attribute, etc.) from the target and transfer it to the caster.
void absorbStat(const ESM::ENAMstruct& effect, const ESM::ActiveEffect& appliedEffect,
const MWWorld::Ptr& caster, const MWWorld::Ptr& target, bool reflected, const std::string& source);
}
#endif

View file

@ -35,7 +35,7 @@
#include "aicombat.hpp" #include "aicombat.hpp"
#include "aipursue.hpp" #include "aipursue.hpp"
#include "spellcasting.hpp" #include "spellutil.hpp"
#include "autocalcspell.hpp" #include "autocalcspell.hpp"
#include "npcstats.hpp" #include "npcstats.hpp"
#include "actorutil.hpp" #include "actorutil.hpp"
@ -390,7 +390,7 @@ namespace MWMechanics
{ {
const std::string& spell = winMgr->getSelectedSpell(); const std::string& spell = winMgr->getSelectedSpell();
if (!spell.empty()) if (!spell.empty())
winMgr->setSelectedSpell(spell, int(MWMechanics::getSpellSuccessChance(spell, mWatched))); winMgr->setSelectedSpell(spell, int(getSpellSuccessChance(spell, mWatched)));
else else
winMgr->unsetSelectedSpell(); winMgr->unsetSelectedSpell();
} }

View file

@ -0,0 +1,76 @@
#include "spellabsorption.hpp"
#include <components/misc/rng.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
#include "../mwrender/animation.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp"
#include "../mwworld/inventorystore.hpp"
#include "creaturestats.hpp"
namespace MWMechanics
{
class GetAbsorptionProbability : public MWMechanics::EffectSourceVisitor
{
public:
float mProbability{0.f};
GetAbsorptionProbability() = default;
virtual void visit (MWMechanics::EffectKey key,
const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/,
float magnitude, float /*remainingTime*/, float /*totalTime*/)
{
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;
}
}
}
};
bool absorbSpell (const ESM::Spell* spell, const MWWorld::Ptr& caster, const MWWorld::Ptr& target)
{
if (!spell || caster == target || !target.getClass().isActor())
return false;
CreatureStats& stats = target.getClass().getCreatureStats(target);
if (stats.getMagicEffects().get(ESM::MagicEffect::SpellAbsorption).getMagnitude() <= 0.f)
return false;
GetAbsorptionProbability check;
stats.getActiveSpells().visitEffectSources(check);
stats.getSpells().visitEffectSources(check);
if (target.getClass().hasInventoryStore(target))
target.getClass().getInventoryStore(target).visitEffectSources(check);
int chance = check.mProbability * 100;
if (Misc::Rng::roll0to99() >= chance)
return false;
const ESM::Static* absorbStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find("VFX_Absorb");
MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target);
if (animation && !absorbStatic->mModel.empty())
animation->addEffect( "meshes\\" + absorbStatic->mModel, ESM::MagicEffect::SpellAbsorption, false, std::string());
// Magicka is increased by the cost of the spell
DynamicStat<float> magicka = stats.getMagicka();
magicka.setCurrent(magicka.getCurrent() + spell->mData.mCost);
stats.setMagicka(magicka);
return true;
}
}

View file

@ -0,0 +1,20 @@
#ifndef MWMECHANICS_SPELLABSORPTION_H
#define MWMECHANICS_SPELLABSORPTION_H
namespace ESM
{
struct Spell;
}
namespace MWWorld
{
class Ptr;
}
namespace MWMechanics
{
// Try to absorb a spell based on the magnitude of every Spell Absorption effect source on the target.
bool absorbSpell(const ESM::Spell* spell, const MWWorld::Ptr& caster, const MWWorld::Ptr& target);
}
#endif

View file

@ -1,11 +1,7 @@
#include "spellcasting.hpp" #include "spellcasting.hpp"
#include <limits>
#include <iomanip>
#include <components/misc/constants.hpp> #include <components/misc/constants.hpp>
#include <components/misc/rng.hpp> #include <components/misc/rng.hpp>
#include <components/settings/settings.hpp>
/* /*
Start of tes3mp addition Start of tes3mp addition
@ -40,328 +36,22 @@
#include "../mwrender/animation.hpp" #include "../mwrender/animation.hpp"
#include "npcstats.hpp"
#include "actorutil.hpp" #include "actorutil.hpp"
#include "aifollow.hpp" #include "aifollow.hpp"
#include "creaturestats.hpp"
#include "linkedeffects.hpp"
#include "spellabsorption.hpp"
#include "spellresistance.hpp"
#include "spellutil.hpp"
#include "summoning.hpp"
#include "tickableeffects.hpp"
#include "weapontype.hpp" #include "weapontype.hpp"
namespace MWMechanics namespace MWMechanics
{ {
ESM::Skill::SkillEnum spellSchoolToSkill(int school)
{
static std::map<int, ESM::Skill::SkillEnum> schoolSkillMap; // maps spell school to skill id
if (schoolSkillMap.empty())
{
schoolSkillMap[0] = ESM::Skill::Alteration;
schoolSkillMap[1] = ESM::Skill::Conjuration;
schoolSkillMap[3] = ESM::Skill::Illusion;
schoolSkillMap[2] = ESM::Skill::Destruction;
schoolSkillMap[4] = ESM::Skill::Mysticism;
schoolSkillMap[5] = ESM::Skill::Restoration;
}
assert(schoolSkillMap.find(school) != schoolSkillMap.end());
return schoolSkillMap[school];
}
float calcEffectCost(const ESM::ENAMstruct& effect)
{
const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find(effect.mEffectID);
return calcEffectCost(effect, magicEffect);
}
float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect)
{
int minMagn = 1;
int maxMagn = 1;
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude))
{
minMagn = effect.mMagnMin;
maxMagn = effect.mMagnMax;
}
int duration = 1;
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration))
duration = effect.mDuration;
static const float fEffectCostMult = MWBase::Environment::get().getWorld()->getStore()
.get<ESM::GameSetting>().find("fEffectCostMult")->mValue.getFloat();
float x = 0.5 * (std::max(1, minMagn) + std::max(1, maxMagn));
x *= 0.1 * magicEffect->mData.mBaseCost;
x *= 1 + duration;
x += 0.05 * std::max(1, effect.mArea) * magicEffect->mData.mBaseCost;
return x * fEffectCostMult;
}
float calcSpellBaseSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool)
{
// Morrowind for some reason uses a formula slightly different from magicka cost calculation
float y = std::numeric_limits<float>::max();
float lowestSkill = 0;
for (std::vector<ESM::ENAMstruct>::const_iterator it = spell->mEffects.mList.begin(); it != spell->mEffects.mList.end(); ++it)
{
float x = static_cast<float>(it->mDuration);
const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find(
it->mEffectID);
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce))
x = std::max(1.f, x);
x *= 0.1f * magicEffect->mData.mBaseCost;
x *= 0.5f * (it->mMagnMin + it->mMagnMax);
x *= it->mArea * 0.05f * magicEffect->mData.mBaseCost;
if (it->mRange == ESM::RT_Target)
x *= 1.5f;
static const float fEffectCostMult = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find(
"fEffectCostMult")->mValue.getFloat();
x *= fEffectCostMult;
float s = 2.0f * actor.getClass().getSkill(actor, spellSchoolToSkill(magicEffect->mData.mSchool));
if (s - x < y)
{
y = s - x;
if (effectiveSchool)
*effectiveSchool = magicEffect->mData.mSchool;
lowestSkill = s;
}
}
CreatureStats& stats = actor.getClass().getCreatureStats(actor);
int actorWillpower = stats.getAttribute(ESM::Attribute::Willpower).getModified();
int actorLuck = stats.getAttribute(ESM::Attribute::Luck).getModified();
float castChance = (lowestSkill - spell->mData.mCost + 0.2f * actorWillpower + 0.1f * actorLuck);
return castChance;
}
float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka)
{
bool godmode = actor == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState();
CreatureStats& stats = actor.getClass().getCreatureStats(actor);
float castBonus = -stats.getMagicEffects().get(ESM::MagicEffect::Sound).getMagnitude();
float castChance = calcSpellBaseSuccessChance(spell, actor, effectiveSchool) + castBonus;
castChance *= stats.getFatigueTerm();
if (stats.getMagicEffects().get(ESM::MagicEffect::Silence).getMagnitude()&& !godmode)
return 0;
if (spell->mData.mType == ESM::Spell::ST_Power)
return stats.getSpells().canUsePower(spell) ? 100 : 0;
if (spell->mData.mType != ESM::Spell::ST_Spell)
return 100;
if (checkMagicka && stats.getMagicka().getCurrent() < spell->mData.mCost && !godmode)
return 0;
if (spell->mData.mFlags & ESM::Spell::F_Always)
return 100;
if (godmode)
{
return 100;
}
if (!cap)
return std::max(0.f, castChance);
else
return std::max(0.f, std::min(100.f, castChance));
}
float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka)
{
const ESM::Spell* spell =
MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().find(spellId);
return getSpellSuccessChance(spell, actor, effectiveSchool, cap, checkMagicka);
}
int getSpellSchool(const std::string& spellId, const MWWorld::Ptr& actor)
{
int school = 0;
getSpellSuccessChance(spellId, actor, &school);
return school;
}
int getSpellSchool(const ESM::Spell* spell, const MWWorld::Ptr& actor)
{
int school = 0;
getSpellSuccessChance(spell, actor, &school);
return school;
}
bool spellIncreasesSkill(const ESM::Spell *spell)
{
if (spell->mData.mType == ESM::Spell::ST_Spell && !(spell->mData.mFlags & ESM::Spell::F_Always))
return true;
return false;
}
bool spellIncreasesSkill(const std::string &spellId)
{
const ESM::Spell* spell =
MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().find(spellId);
return spellIncreasesSkill(spell);
}
float getEffectResistanceAttribute (short effectId, const MagicEffects* actorEffects)
{
short resistanceEffect = ESM::MagicEffect::getResistanceEffect(effectId);
short weaknessEffect = ESM::MagicEffect::getWeaknessEffect(effectId);
float resistance = 0;
if (resistanceEffect != -1)
resistance += actorEffects->get(resistanceEffect).getMagnitude();
if (weaknessEffect != -1)
resistance -= actorEffects->get(weaknessEffect).getMagnitude();
if (effectId == ESM::MagicEffect::FireDamage)
resistance += actorEffects->get(ESM::MagicEffect::FireShield).getMagnitude();
if (effectId == ESM::MagicEffect::ShockDamage)
resistance += actorEffects->get(ESM::MagicEffect::LightningShield).getMagnitude();
if (effectId == ESM::MagicEffect::FrostDamage)
resistance += actorEffects->get(ESM::MagicEffect::FrostShield).getMagnitude();
return resistance;
}
float getEffectResistance (short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster,
const ESM::Spell* spell, const MagicEffects* effects)
{
const ESM::MagicEffect *magicEffect =
MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find (
effectId);
const MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor);
const MWMechanics::MagicEffects* magicEffects = &stats.getMagicEffects();
if (effects)
magicEffects = effects;
// Effects with no resistance attribute belonging to them can not be resisted
if (ESM::MagicEffect::getResistanceEffect(effectId) == -1)
return 0.f;
float resistance = getEffectResistanceAttribute(effectId, magicEffects);
int willpower = stats.getAttribute(ESM::Attribute::Willpower).getModified();
float luck = static_cast<float>(stats.getAttribute(ESM::Attribute::Luck).getModified());
float x = (willpower + 0.1f * luck) * stats.getFatigueTerm();
// This makes spells that are easy to cast harder to resist and vice versa
float castChance = 100.f;
if (spell != nullptr && !caster.isEmpty() && caster.getClass().isActor())
{
castChance = getSpellSuccessChance(spell, caster, nullptr, false, false); // Uncapped casting chance
}
if (castChance > 0)
x *= 50 / castChance;
float roll = Misc::Rng::rollClosedProbability() * 100;
if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)
roll -= resistance;
if (x <= roll)
x = 0;
else
{
if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)
x = 100;
else
x = roll / std::min(x, 100.f);
}
x = std::min(x + resistance, 100.f);
return x;
}
float getEffectMultiplier(short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster,
const ESM::Spell* spell, const MagicEffects* effects)
{
float resistance = getEffectResistance(effectId, actor, caster, spell, effects);
return 1 - resistance / 100.f;
}
/// Check if the given effect can be applied to the target. If \a castByPlayer, emits a message box on failure.
bool checkEffectTarget (int effectId, const MWWorld::Ptr& target, const MWWorld::Ptr& caster, bool castByPlayer)
{
switch (effectId)
{
case ESM::MagicEffect::Levitate:
if (!MWBase::Environment::get().getWorld()->isLevitationEnabled())
{
if (castByPlayer)
MWBase::Environment::get().getWindowManager()->messageBox("#{sLevitateDisabled}");
return false;
}
break;
case ESM::MagicEffect::Soultrap:
if (!target.getClass().isNpc() // no messagebox for NPCs
&& (target.getTypeName() == typeid(ESM::Creature).name() && target.get<ESM::Creature>()->mBase->mData.mSoul == 0))
{
if (castByPlayer)
MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInvalidTarget}");
return true; // must still apply to get visual effect and have target regard it as attack
}
break;
case ESM::MagicEffect::WaterWalking:
if (target.getClass().isPureWaterCreature(target) && MWBase::Environment::get().getWorld()->isSwimming(target))
return false;
MWBase::World *world = MWBase::Environment::get().getWorld();
if (!world->isWaterWalkingCastableOnTarget(target))
{
if (castByPlayer && caster == target)
MWBase::Environment::get().getWindowManager()->messageBox ("#{sMagicInvalidEffect}");
return false;
}
break;
}
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) CastSpell::CastSpell(const MWWorld::Ptr &caster, const MWWorld::Ptr &target, const bool fromProjectile, const bool manualSpell)
: mCaster(caster) : mCaster(caster)
, mTarget(target) , mTarget(target)
, mStack(false)
, mHitPosition(0,0,0)
, mAlwaysSucceed(false)
, mFromProjectile(fromProjectile) , mFromProjectile(fromProjectile)
, mManualSpell(manualSpell) , mManualSpell(manualSpell)
{ {
@ -392,10 +82,9 @@ namespace MWMechanics
// If none of the effects need to apply, we can early-out // If none of the effects need to apply, we can early-out
bool found = false; bool found = false;
for (std::vector<ESM::ENAMstruct>::const_iterator iter (effects.mList.begin()); for (const ESM::ENAMstruct& effect : effects.mList)
iter!=effects.mList.end(); ++iter)
{ {
if (iter->mRange == range) if (effect.mRange == range)
{ {
found = true; found = true;
break; break;
@ -441,6 +130,9 @@ namespace MWMechanics
// throughout the iteration of this spell's // throughout the iteration of this spell's
// effects, we display a "can't re-cast" message // effects, we display a "can't re-cast" message
// Try absorbing the spell. Some handling must still happen for absorbed effects.
bool absorbed = absorbSpell(spell, caster, target);
for (std::vector<ESM::ENAMstruct>::const_iterator effectIt (effects.mList.begin()); for (std::vector<ESM::ENAMstruct>::const_iterator effectIt (effects.mList.begin());
!target.isEmpty() && effectIt != effects.mList.end(); ++effectIt) !target.isEmpty() && effectIt != effects.mList.end(); ++effectIt)
{ {
@ -458,8 +150,7 @@ namespace MWMechanics
MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCannotRecast}"); MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCannotRecast}");
continue; continue;
} }
else canCastAnEffect = true;
canCastAnEffect = true;
if (!checkEffectTarget(effectIt->mEffectID, target, caster, castByPlayer)) if (!checkEffectTarget(effectIt->mEffectID, target, caster, castByPlayer))
continue; continue;
@ -469,89 +160,30 @@ namespace MWMechanics
&& (caster.isEmpty() || !caster.getClass().isActor())) && (caster.isEmpty() || !caster.getClass().isActor()))
continue; continue;
// If player is healing someone, show the target's HP bar // Notify the target actor they've been hit
if (castByPlayer && target != caster bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful;
&& effectIt->mEffectID == ESM::MagicEffect::RestoreHealth if (target.getClass().isActor() && target != caster && !caster.isEmpty() && isHarmful)
&& target.getClass().isActor()) target.getClass().onHit(target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true);
MWBase::Environment::get().getWindowManager()->setEnemy(target);
// Try absorbing if it's a spell // Avoid proceeding further for absorbed spells.
// Unlike Reflect, this is done once per spell absorption effect source if (absorbed)
bool absorbed = false; continue;
if (spell && caster != target && target.getClass().isActor())
// Reflect harmful effects
if (!reflected && reflectEffect(*effectIt, magicEffect, caster, target, reflectedEffects))
continue;
// Try resisting.
float magnitudeMult = getEffectMultiplier(effectIt->mEffectID, target, caster, spell, &targetEffects);
if (magnitudeMult == 0)
{ {
CreatureStats& stats = target.getClass().getCreatureStats(target); // Fully resisted, show message
if (stats.getMagicEffects().get(ESM::MagicEffect::SpellAbsorption).getMagnitude() > 0.f) if (target == getPlayer())
{ MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}");
GetAbsorptionProbability check(target); else if (castByPlayer)
stats.getActiveSpells().visitEffectSources(check); MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}");
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)
{
const ESM::Static* absorbStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find ("VFX_Absorb");
MWBase::Environment::get().getWorld()->getAnimation(target)->addEffect(
"meshes\\" + absorbStatic->mModel, ESM::MagicEffect::SpellAbsorption, false, "");
// Magicka is increased by cost of spell
DynamicStat<float> magicka = stats.getMagicka();
magicka.setCurrent(magicka.getCurrent() + spell->mData.mCost);
stats.setMagicka(magicka);
}
}
} }
else
float magnitudeMult = 1;
if (target.getClass().isActor())
{
if (absorbed)
continue;
bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful;
// Reflect harmful effects
if (isHarmful && !reflected && !caster.isEmpty() && caster != target && !(magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable))
{
float reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).getMagnitude();
bool isReflected = (Misc::Rng::roll0to99() < reflect);
if (isReflected)
{
const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find ("VFX_Reflect");
MWBase::Environment::get().getWorld()->getAnimation(target)->addEffect(
"meshes\\" + reflectStatic->mModel, ESM::MagicEffect::Reflect, false, "");
reflectedEffects.mList.push_back(*effectIt);
continue;
}
}
// Try resisting
magnitudeMult = MWMechanics::getEffectMultiplier(effectIt->mEffectID, target, caster, spell, &targetEffects);
if (magnitudeMult == 0)
{
// Fully resisted, show message
if (target == getPlayer())
MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}");
else if (castByPlayer)
MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}");
}
else if (isHarmful && castByPlayer && target != caster)
{
// If player is attempting to cast a harmful spell and it wasn't fully resisted, show the target's HP bar
MWBase::Environment::get().getWindowManager()->setEnemy(target);
}
if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState() && isHarmful)
magnitudeMult = 0;
// Notify the target actor they've been hit
if (target != caster && !caster.isEmpty() && isHarmful)
target.getClass().onHit(target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true);
}
if (magnitudeMult > 0 && !absorbed)
{ {
float magnitude = effectIt->mMagnMin + Misc::Rng::rollDice(effectIt->mMagnMax - effectIt->mMagnMin + 1); float magnitude = effectIt->mMagnMin + Misc::Rng::rollDice(effectIt->mMagnMax - effectIt->mMagnMin + 1);
magnitude *= magnitudeMult; magnitude *= magnitudeMult;
@ -578,6 +210,19 @@ namespace MWMechanics
effect.mMagnitude = 0; effect.mMagnitude = 0;
} }
// Avoid applying harmful effects to the player in god mode
if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState() && isHarmful)
{
effect.mMagnitude = 0;
}
bool effectAffectsHealth = isHarmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth;
if (castByPlayer && target != caster && effectAffectsHealth)
{
// If player is attempting to cast a harmful spell or is healing someone, show the target's HP bar.
MWBase::Environment::get().getWindowManager()->setEnemy(target);
}
bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration);
if (hasDuration && effectIt->mDuration == 0) if (hasDuration && effectIt->mDuration == 0)
{ {
@ -587,7 +232,7 @@ namespace MWMechanics
// duration 0 means apply full magnitude instantly // duration 0 means apply full magnitude instantly
bool wasDead = target.getClass().getCreatureStats(target).isDead(); bool wasDead = target.getClass().getCreatureStats(target).isDead();
effectTick(target.getClass().getCreatureStats(target), target, EffectKey(*effectIt), magnitude); effectTick(target.getClass().getCreatureStats(target), target, EffectKey(*effectIt), effect.mMagnitude);
bool isDead = target.getClass().getCreatureStats(target).isDead(); bool isDead = target.getClass().getCreatureStats(target).isDead();
/* /*
@ -619,11 +264,7 @@ namespace MWMechanics
} }
else else
{ {
effect.mDuration = hasDuration ? static_cast<float>(effectIt->mDuration) : 1.f;
if (!hasDuration)
effect.mDuration = 1.0f;
else
effect.mDuration = static_cast<float>(effectIt->mDuration);
targetEffects.add(MWMechanics::EffectKey(*effectIt), MWMechanics::EffectParam(effect.mMagnitude)); targetEffects.add(MWMechanics::EffectKey(*effectIt), MWMechanics::EffectParam(effect.mMagnitude));
@ -640,7 +281,7 @@ namespace MWMechanics
// Command spells should have their effect, including taking the target out of combat, each time the spell successfully affects the target // Command spells should have their effect, including taking the target out of combat, each time the spell successfully affects the target
if (((effectIt->mEffectID == ESM::MagicEffect::CommandHumanoid && target.getClass().isNpc()) if (((effectIt->mEffectID == ESM::MagicEffect::CommandHumanoid && target.getClass().isNpc())
|| (effectIt->mEffectID == ESM::MagicEffect::CommandCreature && target.getTypeName() == typeid(ESM::Creature).name())) || (effectIt->mEffectID == ESM::MagicEffect::CommandCreature && target.getTypeName() == typeid(ESM::Creature).name()))
&& !caster.isEmpty() && caster.getClass().isActor() && target != getPlayer() && magnitude >= target.getClass().getCreatureStats(target).getLevel()) && !caster.isEmpty() && caster.getClass().isActor() && target != getPlayer() && effect.mMagnitude >= target.getClass().getCreatureStats(target).getLevel())
{ {
MWMechanics::AiFollow package(caster, true); MWMechanics::AiFollow package(caster, true);
target.getClass().getCreatureStats(target).getAiSequence().stack(package, target); target.getClass().getCreatureStats(target).getAiSequence().stack(package, target);
@ -648,23 +289,8 @@ namespace MWMechanics
// For absorb effects, also apply the effect to the caster - but with a negative // For absorb effects, also apply the effect to the caster - but with a negative
// magnitude, since we're transferring stats from the target to the caster // magnitude, since we're transferring stats from the target to the caster
if (!caster.isEmpty() && caster != target && caster.getClass().isActor()) if (effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute && effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill)
{ absorbStat(*effectIt, effect, caster, target, reflected, mSourceName);
if (effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute &&
effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill)
{
std::vector<ActiveSpells::ActiveEffect> absorbEffects;
ActiveSpells::ActiveEffect effect_ = effect;
effect_.mMagnitude *= -1;
absorbEffects.push_back(effect_);
if (reflected && Settings::Manager::getBool("classic reflected absorb spells behavior", "Game"))
target.getClass().getCreatureStats(target).getActiveSpells().addSpell("", true,
absorbEffects, mSourceName, caster.getClass().getCreatureStats(caster).getActorId());
else
caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell("", true,
absorbEffects, mSourceName, target.getClass().getCreatureStats(target).getActorId());
}
}
} }
} }
@ -712,13 +338,11 @@ namespace MWMechanics
else else
castStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find ("VFX_DefaultHit"); castStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find ("VFX_DefaultHit");
std::string texture = magicEffect->mParticle;
bool loop = (magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; bool loop = (magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0;
// Note: in case of non actor, a free effect should be fine as well // Note: in case of non actor, a free effect should be fine as well
MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(target); MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(target);
if (anim) if (anim && !castStatic->mModel.empty())
anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, loop, "", texture); anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, loop, "", magicEffect->mParticle);
} }
} }
} }
@ -920,16 +544,14 @@ namespace MWMechanics
bool CastSpell::cast(const std::string &id) bool CastSpell::cast(const std::string &id)
{ {
if (const ESM::Spell *spell = const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore();
MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().search (id)) if (const auto spell = store.get<ESM::Spell>().search(id))
return cast(spell); return cast(spell);
if (const ESM::Potion *potion = if (const auto potion = store.get<ESM::Potion>().search(id))
MWBase::Environment::get().getWorld()->getStore().get<ESM::Potion>().search (id))
return cast(potion); return cast(potion);
if (const ESM::Ingredient *ingredient = if (const auto ingredient = store.get<ESM::Ingredient>().search(id))
MWBase::Environment::get().getWorld()->getStore().get<ESM::Ingredient>().search (id))
return cast(ingredient); return cast(ingredient);
throw std::runtime_error("ID type cannot be casted"); throw std::runtime_error("ID type cannot be casted");
@ -1114,13 +736,12 @@ namespace MWMechanics
stats.getSpells().usePower(spell); stats.getSpells().usePower(spell);
} }
if (mCaster == getPlayer() && spellIncreasesSkill()) if (!mManualSpell && mCaster == getPlayer() && spellIncreasesSkill(spell))
mCaster.getClass().skillUsageSucceeded(mCaster, mCaster.getClass().skillUsageSucceeded(mCaster, spellSchoolToSkill(school), 0);
spellSchoolToSkill(school), 0);
// A non-actor doesn't play its spell cast effects from a character controller, so play them here // A non-actor doesn't play its spell cast effects from a character controller, so play them here
if (!mCaster.getClass().isActor()) if (!mCaster.getClass().isActor())
playSpellCastingEffects(mId, false); playSpellCastingEffects(spell->mEffects.mList);
inflict(mCaster, mCaster, spell->mEffects, ESM::RT_Self); inflict(mCaster, mCaster, spell->mEffects, ESM::RT_Self);
@ -1145,10 +766,8 @@ namespace MWMechanics
effect.mRange = ESM::RT_Self; effect.mRange = ESM::RT_Self;
effect.mArea = 0; effect.mArea = 0;
const ESM::MagicEffect *magicEffect = const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore();
MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find ( const auto magicEffect = store.get<ESM::MagicEffect>().find(effect.mEffectID);
effect.mEffectID);
const MWMechanics::CreatureStats& creatureStats = mCaster.getClass().getCreatureStats(mCaster); const MWMechanics::CreatureStats& creatureStats = mCaster.getClass().getCreatureStats(mCaster);
float x = (mCaster.getClass().getSkill(mCaster, ESM::Skill::Alchemy) + float x = (mCaster.getClass().getSkill(mCaster, ESM::Skill::Alchemy) +
@ -1160,7 +779,7 @@ namespace MWMechanics
if (roll > x) if (roll > x)
{ {
// "X has no effect on you" // "X has no effect on you"
std::string message = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("sNotifyMessage50")->mValue.getString(); std::string message = store.get<ESM::GameSetting>().find("sNotifyMessage50")->mValue.getString();
message = Misc::StringUtils::format(message, ingredient->mName); message = Misc::StringUtils::format(message, ingredient->mName);
MWBase::Environment::get().getWindowManager()->messageBox(message); MWBase::Environment::get().getWindowManager()->messageBox(message);
return false; return false;
@ -1200,14 +819,13 @@ namespace MWMechanics
const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore();
if (enchantment) if (enchantment)
{ {
const ESM::Enchantment *spell = store.get<ESM::Enchantment>().find(spellid); if (const auto spell = store.get<ESM::Enchantment>().search(spellid))
playSpellCastingEffects(spell->mEffects.mList); playSpellCastingEffects(spell->mEffects.mList);
} }
else else
{ {
const ESM::Spell *spell = store.get<ESM::Spell>().find(spellid); if (const auto spell = store.get<ESM::Spell>().search(spellid))
playSpellCastingEffects(spell->mEffects.mList); playSpellCastingEffects(spell->mEffects.mList);
} }
} }
@ -1215,12 +833,9 @@ namespace MWMechanics
{ {
const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore();
std::vector<std::string> addedEffects; std::vector<std::string> addedEffects;
for (std::vector<ESM::ENAMstruct>::const_iterator iter = effects.begin(); iter != effects.end(); ++iter) for (const ESM::ENAMstruct& effectData : effects)
{ {
const ESM::MagicEffect *effect; const auto effect = store.get<ESM::MagicEffect>().find(effectData.mEffectID);
effect = store.get<ESM::MagicEffect>().find(iter->mEffectID);
MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(mCaster);
const ESM::Static* castStatic; const ESM::Static* castStatic;
@ -1233,13 +848,10 @@ namespace MWMechanics
if (std::find(addedEffects.begin(), addedEffects.end(), "meshes\\" + castStatic->mModel) != addedEffects.end()) if (std::find(addedEffects.begin(), addedEffects.end(), "meshes\\" + castStatic->mModel) != addedEffects.end())
continue; continue;
std::string texture = effect->mParticle; MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(mCaster);
osg::Vec3f pos (mCaster.getRefData().getPosition().asVec3());
if (animation) if (animation)
{ {
animation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex, false, "", texture); animation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex, false, "", effect->mParticle);
} }
else else
{ {
@ -1248,6 +860,7 @@ namespace MWMechanics
osg::Vec3f bounds (MWBase::Environment::get().getWorld()->getHalfExtents(mCaster) * 2.f / Constants::UnitsPerFoot); osg::Vec3f bounds (MWBase::Environment::get().getWorld()->getHalfExtents(mCaster) * 2.f / Constants::UnitsPerFoot);
float scale = std::max({ bounds.x()/3.f, bounds.y()/3.f, bounds.z()/6.f }); float scale = std::max({ bounds.x()/3.f, bounds.y()/3.f, bounds.z()/6.f });
float meshScale = !mCaster.getClass().isActor() ? mCaster.getCellRef().getScale() : 1.0f; float meshScale = !mCaster.getClass().isActor() ? mCaster.getCellRef().getScale() : 1.0f;
osg::Vec3f pos (mCaster.getRefData().getPosition().asVec3());
MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + castStatic->mModel, effect->mParticle, pos, scale * meshScale); MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + castStatic->mModel, effect->mParticle, pos, scale * meshScale);
} }
@ -1267,308 +880,4 @@ namespace MWMechanics
sndMgr->playSound3D(mCaster, schools[effect->mData.mSchool]+" cast", 1.0f, 1.0f); sndMgr->playSound3D(mCaster, schools[effect->mData.mSchool]+" cast", 1.0f, 1.0f);
} }
} }
bool CastSpell::spellIncreasesSkill()
{
if (mManualSpell)
return false;
return MWMechanics::spellIncreasesSkill(mId);
}
int getEffectiveEnchantmentCastCost(float castCost, const MWWorld::Ptr &actor)
{
/*
* Each point of enchant skill above/under 10 subtracts/adds
* one percent of enchantment cost while minimum is 1.
*/
int eSkill = actor.getClass().getSkill(actor, ESM::Skill::Enchant);
const float result = castCost - (castCost / 100) * (eSkill - 10);
return static_cast<int>((result < 1) ? 1 : result);
}
bool isSummoningEffect(int effectId)
{
return ((effectId >= ESM::MagicEffect::SummonScamp
&& effectId <= ESM::MagicEffect::SummonStormAtronach)
|| effectId == ESM::MagicEffect::SummonCenturionSphere
|| (effectId >= ESM::MagicEffect::SummonFabricant
&& effectId <= ESM::MagicEffect::SummonCreature05));
}
bool disintegrateSlot (MWWorld::Ptr ptr, int slot, float disintegrate)
{
if (ptr.getClass().hasInventoryStore(ptr))
{
MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr);
MWWorld::ContainerStoreIterator item =
inv.getSlot(slot);
if (item != inv.end() && (item.getType() == MWWorld::ContainerStore::Type_Armor || item.getType() == MWWorld::ContainerStore::Type_Weapon))
{
if (!item->getClass().hasItemHealth(*item))
return false;
int charge = item->getClass().getItemHealth(*item);
if (charge == 0)
return false;
// Store remainder of disintegrate amount (automatically subtracted if > 1)
item->getCellRef().applyChargeRemainderToBeSubtracted(disintegrate - std::floor(disintegrate));
charge = item->getClass().getItemHealth(*item);
charge -=
std::min(static_cast<int>(disintegrate),
charge);
item->getCellRef().setCharge(charge);
if (charge == 0)
{
// Will unequip the broken item and try to find a replacement
if (ptr != getPlayer())
inv.autoEquip(ptr);
else
inv.unequipItem(*item, ptr);
}
return true;
}
}
return false;
}
void adjustDynamicStat(CreatureStats& creatureStats, int index, float magnitude, bool allowDecreaseBelowZero = false)
{
DynamicStat<float> stat = creatureStats.getDynamic(index);
stat.setCurrent(stat.getCurrent() + magnitude, allowDecreaseBelowZero);
creatureStats.setDynamic(index, stat);
}
bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey &effectKey, float magnitude)
{
if (magnitude == 0.f)
return false;
bool receivedMagicDamage = false;
switch (effectKey.mId)
{
case ESM::MagicEffect::DamageAttribute:
{
AttributeValue attr = creatureStats.getAttribute(effectKey.mArg);
attr.damage(magnitude);
creatureStats.setAttribute(effectKey.mArg, attr);
break;
}
case ESM::MagicEffect::RestoreAttribute:
{
AttributeValue attr = creatureStats.getAttribute(effectKey.mArg);
attr.restore(magnitude);
creatureStats.setAttribute(effectKey.mArg, attr);
break;
}
case ESM::MagicEffect::RestoreHealth:
case ESM::MagicEffect::RestoreMagicka:
case ESM::MagicEffect::RestoreFatigue:
adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::RestoreHealth, magnitude);
break;
case ESM::MagicEffect::DamageHealth:
receivedMagicDamage = true;
adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::DamageHealth, -magnitude);
break;
case ESM::MagicEffect::DamageMagicka:
case ESM::MagicEffect::DamageFatigue:
{
int index = effectKey.mId-ESM::MagicEffect::DamageHealth;
static const bool uncappedDamageFatigue = Settings::Manager::getBool("uncapped damage fatigue", "Game");
adjustDynamicStat(creatureStats, index, -magnitude, index == 2 && uncappedDamageFatigue);
break;
}
case ESM::MagicEffect::AbsorbHealth:
if (magnitude > 0.f)
receivedMagicDamage = true;
adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude);
break;
case ESM::MagicEffect::AbsorbMagicka:
case ESM::MagicEffect::AbsorbFatigue:
adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude);
break;
case ESM::MagicEffect::DisintegrateArmor:
{
// According to UESP
int priorities[] = {
MWWorld::InventoryStore::Slot_CarriedLeft,
MWWorld::InventoryStore::Slot_Cuirass,
MWWorld::InventoryStore::Slot_LeftPauldron,
MWWorld::InventoryStore::Slot_RightPauldron,
MWWorld::InventoryStore::Slot_LeftGauntlet,
MWWorld::InventoryStore::Slot_RightGauntlet,
MWWorld::InventoryStore::Slot_Helmet,
MWWorld::InventoryStore::Slot_Greaves,
MWWorld::InventoryStore::Slot_Boots
};
for (unsigned int i=0; i<sizeof(priorities)/sizeof(int); ++i)
{
if (disintegrateSlot(actor, priorities[i], magnitude))
break;
}
break;
}
case ESM::MagicEffect::DisintegrateWeapon:
disintegrateSlot(actor, MWWorld::InventoryStore::Slot_CarriedRight, magnitude);
break;
case ESM::MagicEffect::SunDamage:
{
// isInCell shouldn't be needed, but updateActor called during game start
if (!actor.isInCell() || !actor.getCell()->isExterior())
break;
float time = MWBase::Environment::get().getWorld()->getTimeStamp().getHour();
float timeDiff = std::min(7.f, std::max(0.f, std::abs(time - 13)));
float damageScale = 1.f - timeDiff / 7.f;
// When cloudy, the sun damage effect is halved
static float fMagicSunBlockedMult = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find(
"fMagicSunBlockedMult")->mValue.getFloat();
int weather = MWBase::Environment::get().getWorld()->getCurrentWeather();
if (weather > 1)
damageScale *= fMagicSunBlockedMult;
adjustDynamicStat(creatureStats, 0, -magnitude * damageScale);
if (magnitude * damageScale > 0.f)
receivedMagicDamage = true;
break;
}
case ESM::MagicEffect::FireDamage:
case ESM::MagicEffect::ShockDamage:
case ESM::MagicEffect::FrostDamage:
case ESM::MagicEffect::Poison:
{
adjustDynamicStat(creatureStats, 0, -magnitude);
receivedMagicDamage = true;
break;
}
case ESM::MagicEffect::DamageSkill:
case ESM::MagicEffect::RestoreSkill:
{
if (!actor.getClass().isNpc())
break;
NpcStats &npcStats = actor.getClass().getNpcStats(actor);
SkillValue& skill = npcStats.getSkill(effectKey.mArg);
if (effectKey.mId == ESM::MagicEffect::RestoreSkill)
skill.restore(magnitude);
else
skill.damage(magnitude);
break;
}
case ESM::MagicEffect::CurePoison:
actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(ESM::MagicEffect::Poison);
break;
case ESM::MagicEffect::CureParalyzation:
actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(ESM::MagicEffect::Paralyze);
break;
case ESM::MagicEffect::CureCommonDisease:
actor.getClass().getCreatureStats(actor).getSpells().purgeCommonDisease();
break;
case ESM::MagicEffect::CureBlightDisease:
actor.getClass().getCreatureStats(actor).getSpells().purgeBlightDisease();
break;
case ESM::MagicEffect::CureCorprusDisease:
actor.getClass().getCreatureStats(actor).getSpells().purgeCorprusDisease();
break;
case ESM::MagicEffect::RemoveCurse:
actor.getClass().getCreatureStats(actor).getSpells().purgeCurses();
break;
/*
Start of tes3mp addition
Don't apply paralysis to DedicatedPlayers and DedicatedActors unilterally on this client
*/
case ESM::MagicEffect::Paralyze:
{
if (mwmp::PlayerList::isDedicatedPlayer(actor) || mwmp::Main::get().getCellController()->isDedicatedActor(actor))
{
actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(ESM::MagicEffect::Paralyze);
break;
}
}
/*
End of tes3mp addition
*/
default:
return false;
}
if (receivedMagicDamage && actor == getPlayer())
MWBase::Environment::get().getWindowManager()->activateHitOverlay(false);
return true;
}
std::string getSummonedCreature(int effectId)
{
static std::map<int, std::string> summonMap;
if (summonMap.empty())
{
summonMap[ESM::MagicEffect::SummonAncestralGhost] = "sMagicAncestralGhostID";
summonMap[ESM::MagicEffect::SummonBonelord] = "sMagicBonelordID";
summonMap[ESM::MagicEffect::SummonBonewalker] = "sMagicLeastBonewalkerID";
summonMap[ESM::MagicEffect::SummonCenturionSphere] = "sMagicCenturionSphereID";
summonMap[ESM::MagicEffect::SummonClannfear] = "sMagicClannfearID";
summonMap[ESM::MagicEffect::SummonDaedroth] = "sMagicDaedrothID";
summonMap[ESM::MagicEffect::SummonDremora] = "sMagicDremoraID";
summonMap[ESM::MagicEffect::SummonFabricant] = "sMagicFabricantID";
summonMap[ESM::MagicEffect::SummonFlameAtronach] = "sMagicFlameAtronachID";
summonMap[ESM::MagicEffect::SummonFrostAtronach] = "sMagicFrostAtronachID";
summonMap[ESM::MagicEffect::SummonGoldenSaint] = "sMagicGoldenSaintID";
summonMap[ESM::MagicEffect::SummonGreaterBonewalker] = "sMagicGreaterBonewalkerID";
summonMap[ESM::MagicEffect::SummonHunger] = "sMagicHungerID";
summonMap[ESM::MagicEffect::SummonScamp] = "sMagicScampID";
summonMap[ESM::MagicEffect::SummonSkeletalMinion] = "sMagicSkeletalMinionID";
summonMap[ESM::MagicEffect::SummonStormAtronach] = "sMagicStormAtronachID";
summonMap[ESM::MagicEffect::SummonWingedTwilight] = "sMagicWingedTwilightID";
summonMap[ESM::MagicEffect::SummonWolf] = "sMagicCreature01ID";
summonMap[ESM::MagicEffect::SummonBear] = "sMagicCreature02ID";
summonMap[ESM::MagicEffect::SummonBonewolf] = "sMagicCreature03ID";
summonMap[ESM::MagicEffect::SummonCreature04] = "sMagicCreature04ID";
summonMap[ESM::MagicEffect::SummonCreature05] = "sMagicCreature05ID";
}
std::map<int, std::string>::const_iterator it = summonMap.find(effectId);
if (it == summonMap.end())
return std::string();
else
return MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find(it->second)->mValue.getString();
}
void ApplyLoopingParticlesVisitor::visit (MWMechanics::EffectKey key,
const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/,
float /*magnitude*/, float /*remainingTime*/, float /*totalTime*/)
{
const ESM::MagicEffect *magicEffect =
MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find(key.mId);
const ESM::Static* castStatic;
if (!magicEffect->mHit.empty())
castStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find (magicEffect->mHit);
else
castStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find ("VFX_DefaultHit");
std::string texture = magicEffect->mParticle;
bool loop = (magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0;
MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mActor);
if (anim && loop)
anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, loop, "", texture);
}
} }

View file

@ -1,14 +1,11 @@
#ifndef MWMECHANICS_SPELLSUCCESS_H #ifndef MWMECHANICS_SPELLCASTING_H
#define MWMECHANICS_SPELLSUCCESS_H #define MWMECHANICS_SPELLCASTING_H
#include <components/esm/effectlist.hpp> #include <components/esm/effectlist.hpp>
#include <components/esm/loadskil.hpp>
#include <components/esm/loadmgef.hpp> #include <components/esm/loadmgef.hpp>
#include "../mwworld/ptr.hpp" #include "../mwworld/ptr.hpp"
#include "magiceffects.hpp"
namespace ESM namespace ESM
{ {
struct Spell; struct Spell;
@ -20,63 +17,6 @@ namespace ESM
namespace MWMechanics namespace MWMechanics
{ {
struct EffectKey; struct EffectKey;
class MagicEffects;
class CreatureStats;
ESM::Skill::SkillEnum spellSchoolToSkill(int school);
float calcEffectCost(const ESM::ENAMstruct& effect);
float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect);
bool isSummoningEffect(int effectId);
/**
* @param spell spell to cast
* @param actor calculate spell success chance for this actor (depends on actor's skills)
* @param effectiveSchool the spell's effective school (relevant for skill progress) will be written here
* @param cap cap the result to 100%?
* @param checkMagicka check magicka?
* @note actor can be an NPC or a creature
* @return success chance from 0 to 100 (in percent), if cap=false then chance above 100 may be returned.
*/
float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool = nullptr, bool cap=true, bool checkMagicka=true);
float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool = nullptr, bool cap=true, bool checkMagicka=true);
int getSpellSchool(const std::string& spellId, const MWWorld::Ptr& actor);
int getSpellSchool(const ESM::Spell* spell, const MWWorld::Ptr& actor);
/// Get whether or not the given spell contributes to skill progress.
bool spellIncreasesSkill(const ESM::Spell* spell);
bool spellIncreasesSkill(const std::string& spellId);
/// Get the resistance attribute against an effect for a given actor. This will add together
/// ResistX and Weakness to X effects relevant against the given effect.
float getEffectResistanceAttribute (short effectId, const MagicEffects* actorEffects);
/// Get the effective resistance against an effect casted by the given actor in the given spell (optional).
/// @return >=100 for fully resisted. can also return negative value for damage amplification.
/// @param effects Override the actor's current magicEffects. Useful if there are effects currently
/// being applied (but not applied yet) that should also be considered.
float getEffectResistance (short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster,
const ESM::Spell* spell = nullptr, const MagicEffects* effects = nullptr);
/// Get an effect multiplier for applying an effect cast by the given actor in the given spell (optional).
/// @return effect multiplier from 0 to 2. (100% net resistance to 100% net weakness)
/// @param effects Override the actor's current magicEffects. Useful if there are effects currently
/// being applied (but not applied yet) that should also be considered.
float getEffectMultiplier(short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster,
const ESM::Spell* spell = nullptr, const MagicEffects* effects = nullptr);
bool checkEffectTarget (int effectId, const MWWorld::Ptr& target, const MWWorld::Ptr& caster, bool castByPlayer);
int getEffectiveEnchantmentCastCost (float castCost, const MWWorld::Ptr& actor);
float calcSpellBaseSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool);
/// Apply a magic effect that is applied in tick intervals until its remaining time ends or it is removed
/// @return Was the effect a tickable effect with a magnitude?
bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const MWMechanics::EffectKey& effectKey, float magnitude);
std::string getSummonedCreature(int effectId);
class CastSpell class CastSpell
{ {
@ -87,11 +27,11 @@ namespace MWMechanics
void playSpellCastingEffects(const std::vector<ESM::ENAMstruct>& effects); void playSpellCastingEffects(const std::vector<ESM::ENAMstruct>& effects);
public: public:
bool mStack; bool mStack{false};
std::string mId; // ID of spell, potion, item etc std::string mId; // ID of spell, potion, item etc
std::string mSourceName; // Display name for spell, potion, etc std::string mSourceName; // Display name for spell, potion, etc
osg::Vec3f mHitPosition; // Used for spawning area orb osg::Vec3f mHitPosition{0,0,0}; // Used for spawning area orb
bool mAlwaysSucceed; // Always succeed spells casted by NPCs/creatures regardless of their chance (default: false) bool mAlwaysSucceed{false}; // Always succeed spells casted by NPCs/creatures regardless of their chance (default: false)
bool mFromProjectile; // True if spell is cast by enchantment of some projectile (arrow, bolt or thrown weapon) bool mFromProjectile; // True if spell is cast by enchantment of some projectile (arrow, bolt or thrown weapon)
bool mManualSpell; // True if spell is casted from script and ignores some checks (mana level, success chance, etc.) bool mManualSpell; // True if spell is casted from script and ignores some checks (mana level, success chance, etc.)
@ -114,8 +54,6 @@ namespace MWMechanics
void playSpellCastingEffects(const std::string &spellid, bool enchantment); void playSpellCastingEffects(const std::string &spellid, bool enchantment);
bool spellIncreasesSkill();
/// Launch a bolt with the given effects. /// Launch a bolt with the given effects.
void launchMagicBolt (); void launchMagicBolt ();
@ -128,22 +66,6 @@ namespace MWMechanics
/// @return was the target suitable for the effect? /// @return was the target suitable for the effect?
bool applyInstantEffect (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const MWMechanics::EffectKey& effect, float magnitude); bool applyInstantEffect (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const MWMechanics::EffectKey& effect, float magnitude);
}; };
class ApplyLoopingParticlesVisitor : public EffectSourceVisitor
{
private:
MWWorld::Ptr mActor;
public:
ApplyLoopingParticlesVisitor(const MWWorld::Ptr& actor)
: mActor(actor)
{
}
virtual void visit (MWMechanics::EffectKey key,
const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/,
float /*magnitude*/, float /*remainingTime*/ = -1, float /*totalTime*/ = -1);
};
} }
#endif #endif

View file

@ -15,9 +15,11 @@
#include "../mwworld/cellstore.hpp" #include "../mwworld/cellstore.hpp"
#include "creaturestats.hpp" #include "creaturestats.hpp"
#include "spellcasting.hpp" #include "spellresistance.hpp"
#include "weapontype.hpp" #include "weapontype.hpp"
#include "combat.hpp" #include "combat.hpp"
#include "summoning.hpp"
#include "spellutil.hpp"
namespace namespace
{ {

View file

@ -0,0 +1,93 @@
#include "spellresistance.hpp"
#include <components/misc/rng.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp"
#include "creaturestats.hpp"
#include "spellutil.hpp"
namespace MWMechanics
{
float getEffectMultiplier(short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster,
const ESM::Spell* spell, const MagicEffects* effects)
{
if (!actor.getClass().isActor())
return 1;
float resistance = getEffectResistance(effectId, actor, caster, spell, effects);
return 1 - resistance / 100.f;
}
float getEffectResistance (short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster,
const ESM::Spell* spell, const MagicEffects* effects)
{
// Effects with no resistance attribute belonging to them can not be resisted
if (ESM::MagicEffect::getResistanceEffect(effectId) == -1)
return 0.f;
const auto magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find(effectId);
const MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor);
const MWMechanics::MagicEffects* magicEffects = &stats.getMagicEffects();
if (effects)
magicEffects = effects;
float resistance = getEffectResistanceAttribute(effectId, magicEffects);
int willpower = stats.getAttribute(ESM::Attribute::Willpower).getModified();
float luck = static_cast<float>(stats.getAttribute(ESM::Attribute::Luck).getModified());
float x = (willpower + 0.1f * luck) * stats.getFatigueTerm();
// This makes spells that are easy to cast harder to resist and vice versa
float castChance = 100.f;
if (spell != nullptr && !caster.isEmpty() && caster.getClass().isActor())
castChance = getSpellSuccessChance(spell, caster, nullptr, false, false); // Uncapped casting chance
if (castChance > 0)
x *= 50 / castChance;
float roll = Misc::Rng::rollClosedProbability() * 100;
if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)
roll -= resistance;
if (x <= roll)
x = 0;
else
{
if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)
x = 100;
else
x = roll / std::min(x, 100.f);
}
x = std::min(x + resistance, 100.f);
return x;
}
float getEffectResistanceAttribute (short effectId, const MagicEffects* actorEffects)
{
short resistanceEffect = ESM::MagicEffect::getResistanceEffect(effectId);
short weaknessEffect = ESM::MagicEffect::getWeaknessEffect(effectId);
float resistance = 0;
if (resistanceEffect != -1)
resistance += actorEffects->get(resistanceEffect).getMagnitude();
if (weaknessEffect != -1)
resistance -= actorEffects->get(weaknessEffect).getMagnitude();
if (effectId == ESM::MagicEffect::FireDamage)
resistance += actorEffects->get(ESM::MagicEffect::FireShield).getMagnitude();
if (effectId == ESM::MagicEffect::ShockDamage)
resistance += actorEffects->get(ESM::MagicEffect::LightningShield).getMagnitude();
if (effectId == ESM::MagicEffect::FrostDamage)
resistance += actorEffects->get(ESM::MagicEffect::FrostShield).getMagnitude();
return resistance;
}
}

View file

@ -0,0 +1,37 @@
#ifndef MWMECHANICS_SPELLRESISTANCE_H
#define MWMECHANICS_SPELLRESISTANCE_H
namespace ESM
{
struct Spell;
}
namespace MWWorld
{
class Ptr;
}
namespace MWMechanics
{
class MagicEffects;
/// Get an effect multiplier for applying an effect cast by the given actor in the given spell (optional).
/// @return effect multiplier from 0 to 2. (100% net resistance to 100% net weakness)
/// @param effects Override the actor's current magicEffects. Useful if there are effects currently
/// being applied (but not applied yet) that should also be considered.
float getEffectMultiplier(short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster,
const ESM::Spell* spell = nullptr, const MagicEffects* effects = nullptr);
/// Get the effective resistance against an effect casted by the given actor in the given spell (optional).
/// @return >=100 for fully resisted. can also return negative value for damage amplification.
/// @param effects Override the actor's current magicEffects. Useful if there are effects currently
/// being applied (but not applied yet) that should also be considered.
float getEffectResistance (short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster,
const ESM::Spell* spell = nullptr, const MagicEffects* effects = nullptr);
/// Get the resistance attribute against an effect for a given actor. This will add together
/// ResistX and Weakness to X effects relevant against the given effect.
float getEffectResistanceAttribute (short effectId, const MagicEffects* actorEffects);
}
#endif

View file

@ -0,0 +1,208 @@
#include "spellutil.hpp"
#include <limits>
#include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwbase/world.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp"
#include "actorutil.hpp"
#include "creaturestats.hpp"
namespace MWMechanics
{
ESM::Skill::SkillEnum spellSchoolToSkill(int school)
{
static const std::array<ESM::Skill::SkillEnum, 6> schoolSkillArray
{
ESM::Skill::Alteration, ESM::Skill::Conjuration, ESM::Skill::Destruction,
ESM::Skill::Illusion, ESM::Skill::Mysticism, ESM::Skill::Restoration
};
return schoolSkillArray.at(school);
}
float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect)
{
const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore();
if (!magicEffect)
magicEffect = store.get<ESM::MagicEffect>().find(effect.mEffectID);
bool hasMagnitude = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude);
bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration);
int minMagn = hasMagnitude ? effect.mMagnMin : 1;
int maxMagn = hasMagnitude ? effect.mMagnMax : 1;
int duration = hasDuration ? effect.mDuration : 1;
static const float fEffectCostMult = store.get<ESM::GameSetting>().find("fEffectCostMult")->mValue.getFloat();
float x = 0.5 * (std::max(1, minMagn) + std::max(1, maxMagn));
x *= 0.1 * magicEffect->mData.mBaseCost;
x *= 1 + duration;
x += 0.05 * std::max(1, effect.mArea) * magicEffect->mData.mBaseCost;
return x * fEffectCostMult;
}
int getEffectiveEnchantmentCastCost(float castCost, const MWWorld::Ptr &actor)
{
/*
* Each point of enchant skill above/under 10 subtracts/adds
* one percent of enchantment cost while minimum is 1.
*/
int eSkill = actor.getClass().getSkill(actor, ESM::Skill::Enchant);
const float result = castCost - (castCost / 100) * (eSkill - 10);
return static_cast<int>((result < 1) ? 1 : result);
}
float calcSpellBaseSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool)
{
// Morrowind for some reason uses a formula slightly different from magicka cost calculation
float y = std::numeric_limits<float>::max();
float lowestSkill = 0;
for (const ESM::ENAMstruct& effect : spell->mEffects.mList)
{
float x = static_cast<float>(effect.mDuration);
const auto magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find(effect.mEffectID);
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce))
x = std::max(1.f, x);
x *= 0.1f * magicEffect->mData.mBaseCost;
x *= 0.5f * (effect.mMagnMin + effect.mMagnMax);
x += effect.mArea * 0.05f * magicEffect->mData.mBaseCost;
if (effect.mRange == ESM::RT_Target)
x *= 1.5f;
static const float fEffectCostMult = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find(
"fEffectCostMult")->mValue.getFloat();
x *= fEffectCostMult;
float s = 2.0f * actor.getClass().getSkill(actor, spellSchoolToSkill(magicEffect->mData.mSchool));
if (s - x < y)
{
y = s - x;
if (effectiveSchool)
*effectiveSchool = magicEffect->mData.mSchool;
lowestSkill = s;
}
}
CreatureStats& stats = actor.getClass().getCreatureStats(actor);
int actorWillpower = stats.getAttribute(ESM::Attribute::Willpower).getModified();
int actorLuck = stats.getAttribute(ESM::Attribute::Luck).getModified();
float castChance = (lowestSkill - spell->mData.mCost + 0.2f * actorWillpower + 0.1f * actorLuck);
return castChance;
}
float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka)
{
bool godmode = actor == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState();
CreatureStats& stats = actor.getClass().getCreatureStats(actor);
if (stats.getMagicEffects().get(ESM::MagicEffect::Silence).getMagnitude() && !godmode)
return 0;
if (spell->mData.mType == ESM::Spell::ST_Power)
return stats.getSpells().canUsePower(spell) ? 100 : 0;
if (godmode)
return 100;
if (spell->mData.mType != ESM::Spell::ST_Spell)
return 100;
if (checkMagicka && stats.getMagicka().getCurrent() < spell->mData.mCost)
return 0;
if (spell->mData.mFlags & ESM::Spell::F_Always)
return 100;
float castBonus = -stats.getMagicEffects().get(ESM::MagicEffect::Sound).getMagnitude();
float castChance = calcSpellBaseSuccessChance(spell, actor, effectiveSchool) + castBonus;
castChance *= stats.getFatigueTerm();
return std::max(0.f, cap ? std::min(100.f, castChance) : castChance);
}
float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka)
{
if (const auto spell = MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().search(spellId))
return getSpellSuccessChance(spell, actor, effectiveSchool, cap, checkMagicka);
return 0.f;
}
int getSpellSchool(const std::string& spellId, const MWWorld::Ptr& actor)
{
int school = 0;
getSpellSuccessChance(spellId, actor, &school);
return school;
}
int getSpellSchool(const ESM::Spell* spell, const MWWorld::Ptr& actor)
{
int school = 0;
getSpellSuccessChance(spell, actor, &school);
return school;
}
bool spellIncreasesSkill(const ESM::Spell *spell)
{
return spell->mData.mType == ESM::Spell::ST_Spell && !(spell->mData.mFlags & ESM::Spell::F_Always);
}
bool spellIncreasesSkill(const std::string &spellId)
{
const auto spell = MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().search(spellId);
return spell && spellIncreasesSkill(spell);
}
bool checkEffectTarget (int effectId, const MWWorld::Ptr& target, const MWWorld::Ptr& caster, bool castByPlayer)
{
switch (effectId)
{
case ESM::MagicEffect::Levitate:
{
if (!MWBase::Environment::get().getWorld()->isLevitationEnabled())
{
if (castByPlayer)
MWBase::Environment::get().getWindowManager()->messageBox("#{sLevitateDisabled}");
return false;
}
break;
}
case ESM::MagicEffect::Soultrap:
{
if (!target.getClass().isNpc() // no messagebox for NPCs
&& (target.getTypeName() == typeid(ESM::Creature).name() && target.get<ESM::Creature>()->mBase->mData.mSoul == 0))
{
if (castByPlayer)
MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInvalidTarget}");
return true; // must still apply to get visual effect and have target regard it as attack
}
break;
}
case ESM::MagicEffect::WaterWalking:
{
if (target.getClass().isPureWaterCreature(target) && MWBase::Environment::get().getWorld()->isSwimming(target))
return false;
MWBase::World *world = MWBase::Environment::get().getWorld();
if (!world->isWaterWalkingCastableOnTarget(target))
{
if (castByPlayer && caster == target)
MWBase::Environment::get().getWindowManager()->messageBox ("#{sMagicInvalidEffect}");
return false;
}
break;
}
}
return true;
}
}

View file

@ -0,0 +1,50 @@
#ifndef MWMECHANICS_SPELLUTIL_H
#define MWMECHANICS_SPELLUTIL_H
#include <components/esm/loadskil.hpp>
namespace ESM
{
struct ENAMstruct;
struct MagicEffect;
struct Spell;
}
namespace MWWorld
{
class Ptr;
}
namespace MWMechanics
{
ESM::Skill::SkillEnum spellSchoolToSkill(int school);
float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect = nullptr);
int getEffectiveEnchantmentCastCost (float castCost, const MWWorld::Ptr& actor);
/**
* @param spell spell to cast
* @param actor calculate spell success chance for this actor (depends on actor's skills)
* @param effectiveSchool the spell's effective school (relevant for skill progress) will be written here
* @param cap cap the result to 100%?
* @param checkMagicka check magicka?
* @note actor can be an NPC or a creature
* @return success chance from 0 to 100 (in percent), if cap=false then chance above 100 may be returned.
*/
float calcSpellBaseSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool);
float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool = nullptr, bool cap=true, bool checkMagicka=true);
float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool = nullptr, bool cap=true, bool checkMagicka=true);
int getSpellSchool(const std::string& spellId, const MWWorld::Ptr& actor);
int getSpellSchool(const ESM::Spell* spell, const MWWorld::Ptr& actor);
/// Get whether or not the given spell contributes to skill progress.
bool spellIncreasesSkill(const ESM::Spell* spell);
bool spellIncreasesSkill(const std::string& spellId);
/// Check if the given effect can be applied to the target. If \a castByPlayer, emits a message box on failure.
bool checkEffectTarget (int effectId, const MWWorld::Ptr& target, const MWWorld::Ptr& caster, bool castByPlayer);
}
#endif

View file

@ -28,20 +28,55 @@
#include "../mwrender/animation.hpp" #include "../mwrender/animation.hpp"
#include "spellcasting.hpp"
#include "creaturestats.hpp" #include "creaturestats.hpp"
#include "aifollow.hpp" #include "aifollow.hpp"
namespace MWMechanics namespace MWMechanics
{ {
UpdateSummonedCreatures::UpdateSummonedCreatures(const MWWorld::Ptr &actor) bool isSummoningEffect(int effectId)
: mActor(actor)
{ {
return ((effectId >= ESM::MagicEffect::SummonScamp && effectId <= ESM::MagicEffect::SummonStormAtronach)
|| (effectId == ESM::MagicEffect::SummonCenturionSphere)
|| (effectId >= ESM::MagicEffect::SummonFabricant && effectId <= ESM::MagicEffect::SummonCreature05));
} }
UpdateSummonedCreatures::~UpdateSummonedCreatures() std::string getSummonedCreature(int effectId)
{
static const std::map<int, std::string> summonMap
{
{ESM::MagicEffect::SummonAncestralGhost, "sMagicAncestralGhostID"},
{ESM::MagicEffect::SummonBonelord, "sMagicBonelordID"},
{ESM::MagicEffect::SummonBonewalker, "sMagicLeastBonewalkerID"},
{ESM::MagicEffect::SummonCenturionSphere, "sMagicCenturionSphereID"},
{ESM::MagicEffect::SummonClannfear, "sMagicClannfearID"},
{ESM::MagicEffect::SummonDaedroth, "sMagicDaedrothID"},
{ESM::MagicEffect::SummonDremora, "sMagicDremoraID"},
{ESM::MagicEffect::SummonFabricant, "sMagicFabricantID"},
{ESM::MagicEffect::SummonFlameAtronach, "sMagicFlameAtronachID"},
{ESM::MagicEffect::SummonFrostAtronach, "sMagicFrostAtronachID"},
{ESM::MagicEffect::SummonGoldenSaint, "sMagicGoldenSaintID"},
{ESM::MagicEffect::SummonGreaterBonewalker, "sMagicGreaterBonewalkerID"},
{ESM::MagicEffect::SummonHunger, "sMagicHungerID"},
{ESM::MagicEffect::SummonScamp, "sMagicScampID"},
{ESM::MagicEffect::SummonSkeletalMinion, "sMagicSkeletalMinionID"},
{ESM::MagicEffect::SummonStormAtronach, "sMagicStormAtronachID"},
{ESM::MagicEffect::SummonWingedTwilight, "sMagicWingedTwilightID"},
{ESM::MagicEffect::SummonWolf, "sMagicCreature01ID"},
{ESM::MagicEffect::SummonBear, "sMagicCreature02ID"},
{ESM::MagicEffect::SummonBonewolf, "sMagicCreature03ID"},
{ESM::MagicEffect::SummonCreature04, "sMagicCreature04ID"},
{ESM::MagicEffect::SummonCreature05, "sMagicCreature05ID"}
};
auto it = summonMap.find(effectId);
if (it != summonMap.end())
return MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find(it->second)->mValue.getString();
return std::string();
}
UpdateSummonedCreatures::UpdateSummonedCreatures(const MWWorld::Ptr &actor)
: mActor(actor)
{ {
} }

View file

@ -9,13 +9,16 @@
namespace MWMechanics namespace MWMechanics
{ {
class CreatureStats; class CreatureStats;
bool isSummoningEffect(int effectId);
std::string getSummonedCreature(int effectId);
struct UpdateSummonedCreatures : public EffectSourceVisitor struct UpdateSummonedCreatures : public EffectSourceVisitor
{ {
UpdateSummonedCreatures(const MWWorld::Ptr& actor); UpdateSummonedCreatures(const MWWorld::Ptr& actor);
virtual ~UpdateSummonedCreatures(); virtual ~UpdateSummonedCreatures() = default;
virtual void visit (MWMechanics::EffectKey key, virtual void visit (MWMechanics::EffectKey key,
const std::string& sourceName, const std::string& sourceId, int casterActorId, const std::string& sourceName, const std::string& sourceId, int casterActorId,

View file

@ -0,0 +1,245 @@
#include "tickableeffects.hpp"
#include <components/settings/settings.hpp>
/*
Start of tes3mp addition
Include additional headers for multiplayer purposes
*/
#include "../mwmp/Main.hpp"
#include "../mwmp/PlayerList.hpp"
#include "../mwmp/CellController.hpp"
/*
End of tes3mp addition
*/
#include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwbase/world.hpp"
#include "../mwworld/cellstore.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/containerstore.hpp"
#include "../mwworld/esmstore.hpp"
#include "../mwworld/inventorystore.hpp"
#include "actorutil.hpp"
#include "npcstats.hpp"
namespace MWMechanics
{
void adjustDynamicStat(CreatureStats& creatureStats, int index, float magnitude, bool allowDecreaseBelowZero = false)
{
DynamicStat<float> stat = creatureStats.getDynamic(index);
stat.setCurrent(stat.getCurrent() + magnitude, allowDecreaseBelowZero);
creatureStats.setDynamic(index, stat);
}
bool disintegrateSlot (const MWWorld::Ptr& ptr, int slot, float disintegrate)
{
if (!ptr.getClass().hasInventoryStore(ptr))
return false;
MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr);
MWWorld::ContainerStoreIterator item = inv.getSlot(slot);
if (item != inv.end() && (item.getType() == MWWorld::ContainerStore::Type_Armor || item.getType() == MWWorld::ContainerStore::Type_Weapon))
{
if (!item->getClass().hasItemHealth(*item))
return false;
int charge = item->getClass().getItemHealth(*item);
if (charge == 0)
return false;
// Store remainder of disintegrate amount (automatically subtracted if > 1)
item->getCellRef().applyChargeRemainderToBeSubtracted(disintegrate - std::floor(disintegrate));
charge = item->getClass().getItemHealth(*item);
charge -= std::min(static_cast<int>(disintegrate), charge);
item->getCellRef().setCharge(charge);
if (charge == 0)
{
// Will unequip the broken item and try to find a replacement
if (ptr != getPlayer())
inv.autoEquip(ptr);
else
inv.unequipItem(*item, ptr);
}
return true;
}
return false;
}
bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey &effectKey, float magnitude)
{
if (magnitude == 0.f)
return false;
bool receivedMagicDamage = false;
switch (effectKey.mId)
{
case ESM::MagicEffect::DamageAttribute:
{
AttributeValue attr = creatureStats.getAttribute(effectKey.mArg);
attr.damage(magnitude);
creatureStats.setAttribute(effectKey.mArg, attr);
break;
}
case ESM::MagicEffect::RestoreAttribute:
{
AttributeValue attr = creatureStats.getAttribute(effectKey.mArg);
attr.restore(magnitude);
creatureStats.setAttribute(effectKey.mArg, attr);
break;
}
case ESM::MagicEffect::RestoreHealth:
case ESM::MagicEffect::RestoreMagicka:
case ESM::MagicEffect::RestoreFatigue:
adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::RestoreHealth, magnitude);
break;
case ESM::MagicEffect::DamageHealth:
receivedMagicDamage = true;
adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::DamageHealth, -magnitude);
break;
case ESM::MagicEffect::DamageMagicka:
case ESM::MagicEffect::DamageFatigue:
{
int index = effectKey.mId-ESM::MagicEffect::DamageHealth;
static const bool uncappedDamageFatigue = Settings::Manager::getBool("uncapped damage fatigue", "Game");
adjustDynamicStat(creatureStats, index, -magnitude, index == 2 && uncappedDamageFatigue);
break;
}
case ESM::MagicEffect::AbsorbHealth:
if (magnitude > 0.f)
receivedMagicDamage = true;
adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude);
break;
case ESM::MagicEffect::AbsorbMagicka:
case ESM::MagicEffect::AbsorbFatigue:
adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude);
break;
case ESM::MagicEffect::DisintegrateArmor:
{
static const std::array<int, 9> priorities
{
MWWorld::InventoryStore::Slot_CarriedLeft,
MWWorld::InventoryStore::Slot_Cuirass,
MWWorld::InventoryStore::Slot_LeftPauldron,
MWWorld::InventoryStore::Slot_RightPauldron,
MWWorld::InventoryStore::Slot_LeftGauntlet,
MWWorld::InventoryStore::Slot_RightGauntlet,
MWWorld::InventoryStore::Slot_Helmet,
MWWorld::InventoryStore::Slot_Greaves,
MWWorld::InventoryStore::Slot_Boots
};
for (const int priority : priorities)
{
if (disintegrateSlot(actor, priority, magnitude))
break;
}
break;
}
case ESM::MagicEffect::DisintegrateWeapon:
disintegrateSlot(actor, MWWorld::InventoryStore::Slot_CarriedRight, magnitude);
break;
case ESM::MagicEffect::SunDamage:
{
// isInCell shouldn't be needed, but updateActor called during game start
if (!actor.isInCell() || !actor.getCell()->isExterior())
break;
float time = MWBase::Environment::get().getWorld()->getTimeStamp().getHour();
float timeDiff = std::min(7.f, std::max(0.f, std::abs(time - 13)));
float damageScale = 1.f - timeDiff / 7.f;
// When cloudy, the sun damage effect is halved
static float fMagicSunBlockedMult = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find(
"fMagicSunBlockedMult")->mValue.getFloat();
int weather = MWBase::Environment::get().getWorld()->getCurrentWeather();
if (weather > 1)
damageScale *= fMagicSunBlockedMult;
adjustDynamicStat(creatureStats, 0, -magnitude * damageScale);
if (magnitude * damageScale > 0.f)
receivedMagicDamage = true;
break;
}
case ESM::MagicEffect::FireDamage:
case ESM::MagicEffect::ShockDamage:
case ESM::MagicEffect::FrostDamage:
case ESM::MagicEffect::Poison:
{
adjustDynamicStat(creatureStats, 0, -magnitude);
receivedMagicDamage = true;
break;
}
case ESM::MagicEffect::DamageSkill:
case ESM::MagicEffect::RestoreSkill:
{
if (!actor.getClass().isNpc())
break;
NpcStats &npcStats = actor.getClass().getNpcStats(actor);
SkillValue& skill = npcStats.getSkill(effectKey.mArg);
if (effectKey.mId == ESM::MagicEffect::RestoreSkill)
skill.restore(magnitude);
else
skill.damage(magnitude);
break;
}
case ESM::MagicEffect::CurePoison:
actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(ESM::MagicEffect::Poison);
break;
case ESM::MagicEffect::CureParalyzation:
actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(ESM::MagicEffect::Paralyze);
break;
case ESM::MagicEffect::CureCommonDisease:
actor.getClass().getCreatureStats(actor).getSpells().purgeCommonDisease();
break;
case ESM::MagicEffect::CureBlightDisease:
actor.getClass().getCreatureStats(actor).getSpells().purgeBlightDisease();
break;
case ESM::MagicEffect::CureCorprusDisease:
actor.getClass().getCreatureStats(actor).getSpells().purgeCorprusDisease();
break;
case ESM::MagicEffect::RemoveCurse:
actor.getClass().getCreatureStats(actor).getSpells().purgeCurses();
break;
/*
Start of tes3mp addition
Don't apply paralysis to DedicatedPlayers and DedicatedActors unilterally on this client
*/
case ESM::MagicEffect::Paralyze:
{
if (mwmp::PlayerList::isDedicatedPlayer(actor) || mwmp::Main::get().getCellController()->isDedicatedActor(actor))
{
actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(ESM::MagicEffect::Paralyze);
break;
}
}
/*
End of tes3mp addition
*/
default:
return false;
}
if (receivedMagicDamage && actor == getPlayer())
MWBase::Environment::get().getWindowManager()->activateHitOverlay(false);
return true;
}
}

View file

@ -0,0 +1,19 @@
#ifndef MWMECHANICS_TICKABLEEFFECTS_H
#define MWMECHANICS_TICKABLEEFFECTS_H
namespace MWWorld
{
class Ptr;
}
namespace MWMechanics
{
class CreatureStats;
struct EffectKey;
/// Apply a magic effect that is applied in tick intervals until its remaining time ends or it is removed
/// @return Was the effect a tickable effect with a magnitude?
bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey& effectKey, float magnitude);
}
#endif

View file

@ -13,7 +13,7 @@
#include "combat.hpp" #include "combat.hpp"
#include "aicombataction.hpp" #include "aicombataction.hpp"
#include "spellpriority.hpp" #include "spellpriority.hpp"
#include "spellcasting.hpp" #include "spellutil.hpp"
#include "weapontype.hpp" #include "weapontype.hpp"
namespace MWMechanics namespace MWMechanics

View file

@ -21,6 +21,7 @@
#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/creaturestats.hpp"
#include "../mwmechanics/mechanicsmanagerimp.hpp" #include "../mwmechanics/mechanicsmanagerimp.hpp"
#include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/spellcasting.hpp"
#include "../mwmechanics/spellutil.hpp"
#include "../mwscript/scriptmanagerimp.hpp" #include "../mwscript/scriptmanagerimp.hpp"

View file

@ -11,6 +11,7 @@
#include "../mwmechanics/combat.hpp" #include "../mwmechanics/combat.hpp"
#include "../mwmechanics/levelledlist.hpp" #include "../mwmechanics/levelledlist.hpp"
#include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/spellcasting.hpp"
#include "../mwmechanics/spellutil.hpp"
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"
#include "../mwworld/inventorystore.hpp" #include "../mwworld/inventorystore.hpp"

View file

@ -16,6 +16,7 @@
#include <components/resource/scenemanager.hpp> #include <components/resource/scenemanager.hpp>
#include <components/resource/keyframemanager.hpp> #include <components/resource/keyframemanager.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/misc/constants.hpp> #include <components/misc/constants.hpp>
#include <components/misc/resourcehelpers.hpp> #include <components/misc/resourcehelpers.hpp>
@ -36,6 +37,8 @@
#include <components/settings/settings.hpp> #include <components/settings/settings.hpp>
#include <components/shader/shadermanager.hpp>
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "../mwworld/esmstore.hpp" #include "../mwworld/esmstore.hpp"
@ -502,8 +505,9 @@ namespace MWRender
class TransparencyUpdater : public SceneUtil::StateSetUpdater class TransparencyUpdater : public SceneUtil::StateSetUpdater
{ {
public: public:
TransparencyUpdater(const float alpha) TransparencyUpdater(const float alpha, osg::ref_ptr<osg::Uniform> shadowUniform)
: mAlpha(alpha) : mAlpha(alpha)
, mShadowUniform(shadowUniform)
{ {
} }
@ -517,6 +521,9 @@ namespace MWRender
{ {
osg::BlendFunc* blendfunc (new osg::BlendFunc); osg::BlendFunc* blendfunc (new osg::BlendFunc);
stateset->setAttributeAndModes(blendfunc, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); stateset->setAttributeAndModes(blendfunc, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE);
// TODO: don't do this anymore once custom shadow renderbin is handling it
if (mShadowUniform)
stateset->addUniform(mShadowUniform);
// FIXME: overriding diffuse/ambient/emissive colors // FIXME: overriding diffuse/ambient/emissive colors
osg::Material* material = new osg::Material; osg::Material* material = new osg::Material;
@ -535,6 +542,7 @@ namespace MWRender
private: private:
float mAlpha; float mAlpha;
osg::ref_ptr<osg::Uniform> mShadowUniform;
}; };
struct Animation::AnimSource struct Animation::AnimSource
@ -1744,7 +1752,7 @@ namespace MWRender
{ {
if (mTransparencyUpdater == nullptr) if (mTransparencyUpdater == nullptr)
{ {
mTransparencyUpdater = new TransparencyUpdater(alpha); mTransparencyUpdater = new TransparencyUpdater(alpha, mResourceSystem->getSceneManager()->getShaderManager().getShadowMapAlphaTestEnableUniform());
mObjectRoot->addUpdateCallback(mTransparencyUpdater); mObjectRoot->addUpdateCallback(mTransparencyUpdater);
} }
else else

View file

@ -266,16 +266,22 @@ void HeadAnimationTime::setBlinkStop(float value)
// ---------------------------------------------------- // ----------------------------------------------------
NpcAnimation::NpcType NpcAnimation::getNpcType() NpcAnimation::NpcType NpcAnimation::getNpcType() const
{ {
const MWWorld::Class &cls = mPtr.getClass(); const MWWorld::Class &cls = mPtr.getClass();
// Dead vampires should typically stay vampires. // Dead vampires should typically stay vampires.
if (mNpcType == Type_Vampire && cls.getNpcStats(mPtr).isDead() && !cls.getNpcStats(mPtr).isWerewolf()) if (mNpcType == Type_Vampire && cls.getNpcStats(mPtr).isDead() && !cls.getNpcStats(mPtr).isWerewolf())
return mNpcType; return mNpcType;
return getNpcType(mPtr);
}
NpcAnimation::NpcType NpcAnimation::getNpcType(const MWWorld::Ptr& ptr)
{
const MWWorld::Class &cls = ptr.getClass();
NpcAnimation::NpcType curType = Type_Normal; NpcAnimation::NpcType curType = Type_Normal;
if (cls.getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude() > 0) if (cls.getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude() > 0)
curType = Type_Vampire; curType = Type_Vampire;
if (cls.getNpcStats(mPtr).isWerewolf()) if (cls.getNpcStats(ptr).isWerewolf())
curType = Type_Werewolf; curType = Type_Werewolf;
return curType; return curType;
@ -326,7 +332,7 @@ NpcAnimation::NpcAnimation(const MWWorld::Ptr& ptr, osg::ref_ptr<osg::Group> par
mViewMode(viewMode), mViewMode(viewMode),
mShowWeapons(false), mShowWeapons(false),
mShowCarriedLeft(true), mShowCarriedLeft(true),
mNpcType(getNpcType()), mNpcType(getNpcType(ptr)),
mFirstPersonFieldOfView(firstPersonFieldOfView), mFirstPersonFieldOfView(firstPersonFieldOfView),
mSoundsDisabled(disableSounds), mSoundsDisabled(disableSounds),
mAccurateAiming(false), mAccurateAiming(false),
@ -1127,7 +1133,7 @@ void NpcAnimation::equipmentChanged()
static const bool shieldSheathing = Settings::Manager::getBool("shield sheathing", "Game"); static const bool shieldSheathing = Settings::Manager::getBool("shield sheathing", "Game");
if (shieldSheathing) if (shieldSheathing)
{ {
int weaptype; int weaptype = ESM::Weapon::None;
MWMechanics::getActiveWeapon(mPtr, &weaptype); MWMechanics::getActiveWeapon(mPtr, &weaptype);
showCarriedLeft(updateCarriedLeftVisible(weaptype)); showCarriedLeft(updateCarriedLeftVisible(weaptype));
} }

View file

@ -74,7 +74,7 @@ private:
void updateNpcBase(); void updateNpcBase();
NpcType getNpcType(); NpcType getNpcType() const;
PartHolderPtr insertBoundedPart(const std::string &model, const std::string &bonename, PartHolderPtr insertBoundedPart(const std::string &model, const std::string &bonename,
const std::string &bonefilter, bool enchantedGlow, osg::Vec4f* glowColor=nullptr); const std::string &bonefilter, bool enchantedGlow, osg::Vec4f* glowColor=nullptr);
@ -94,6 +94,7 @@ private:
static bool isFirstPersonPart(const ESM::BodyPart* bodypart); static bool isFirstPersonPart(const ESM::BodyPart* bodypart);
static bool isFemalePart(const ESM::BodyPart* bodypart); static bool isFemalePart(const ESM::BodyPart* bodypart);
static NpcType getNpcType(const MWWorld::Ptr& ptr);
protected: protected:
virtual void addControllers(); virtual void addControllers();

View file

@ -235,6 +235,8 @@ namespace MWRender
sceneRoot->setLightingMask(Mask_Lighting); sceneRoot->setLightingMask(Mask_Lighting);
mSceneRoot = sceneRoot; mSceneRoot = sceneRoot;
sceneRoot->setStartLight(1); sceneRoot->setStartLight(1);
sceneRoot->setNodeMask(Mask_Scene);
sceneRoot->setName("Scene Root");
int shadowCastingTraversalMask = Mask_Scene; int shadowCastingTraversalMask = Mask_Scene;
if (Settings::Manager::getBool("actor shadows", "Shadows")) if (Settings::Manager::getBool("actor shadows", "Shadows"))
@ -347,9 +349,6 @@ namespace MWRender
defaultMat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); defaultMat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f));
sceneRoot->getOrCreateStateSet()->setAttribute(defaultMat); sceneRoot->getOrCreateStateSet()->setAttribute(defaultMat);
sceneRoot->setNodeMask(Mask_Scene);
sceneRoot->setName("Scene Root");
mSky.reset(new SkyManager(sceneRoot, resourceSystem->getSceneManager())); mSky.reset(new SkyManager(sceneRoot, resourceSystem->getSceneManager()));
mSky->setCamera(mViewer->getCamera()); mSky->setCamera(mViewer->getCamera());
@ -377,6 +376,7 @@ namespace MWRender
mViewer->getCamera()->setCullMask(~(Mask_UpdateVisitor|Mask_SimpleWater)); mViewer->getCamera()->setCullMask(~(Mask_UpdateVisitor|Mask_SimpleWater));
NifOsg::Loader::setHiddenNodeMask(Mask_UpdateVisitor); NifOsg::Loader::setHiddenNodeMask(Mask_UpdateVisitor);
NifOsg::Loader::setIntersectionDisabledNodeMask(Mask_Effect);
mNearClip = Settings::Manager::getFloat("near clip", "Camera"); mNearClip = Settings::Manager::getFloat("near clip", "Camera");
mViewDistance = Settings::Manager::getFloat("viewing distance", "Camera"); mViewDistance = Settings::Manager::getFloat("viewing distance", "Camera");
@ -768,11 +768,10 @@ namespace MWRender
void waitTillDone() void waitTillDone()
{ {
mMutex.lock(); OpenThreads::ScopedLock<OpenThreads::Mutex> lock(mMutex);
if (mDone) if (mDone)
return; return;
mCondition.wait(&mMutex); mCondition.wait(&mMutex);
mMutex.unlock();
} }
mutable OpenThreads::Condition mCondition; mutable OpenThreads::Condition mCondition;

View file

@ -247,24 +247,23 @@ namespace MWScript
template<class R> template<class R>
class OpGetAiSetting : public Interpreter::Opcode0 class OpGetAiSetting : public Interpreter::Opcode0
{ {
int mIndex; MWMechanics::CreatureStats::AiSetting mIndex;
public: public:
OpGetAiSetting(int index) : mIndex(index) {} OpGetAiSetting(MWMechanics::CreatureStats::AiSetting index) : mIndex(index) {}
virtual void execute (Interpreter::Runtime& runtime) virtual void execute (Interpreter::Runtime& runtime)
{ {
MWWorld::Ptr ptr = R()(runtime); MWWorld::Ptr ptr = R()(runtime);
runtime.push(ptr.getClass().getCreatureStats (ptr).getAiSetting ( runtime.push(ptr.getClass().getCreatureStats (ptr).getAiSetting (mIndex).getModified());
(MWMechanics::CreatureStats::AiSetting)mIndex).getModified());
} }
}; };
template<class R> template<class R>
class OpModAiSetting : public Interpreter::Opcode0 class OpModAiSetting : public Interpreter::Opcode0
{ {
int mIndex; MWMechanics::CreatureStats::AiSetting mIndex;
public: public:
OpModAiSetting(int index) : mIndex(index) {} OpModAiSetting(MWMechanics::CreatureStats::AiSetting index) : mIndex(index) {}
virtual void execute (Interpreter::Runtime& runtime) virtual void execute (Interpreter::Runtime& runtime)
{ {
@ -272,19 +271,16 @@ namespace MWScript
Interpreter::Type_Integer value = runtime[0].mInteger; Interpreter::Type_Integer value = runtime[0].mInteger;
runtime.pop(); runtime.pop();
MWMechanics::CreatureStats::AiSetting setting ptr.getClass().getCreatureStats (ptr).setAiSetting (mIndex,
= MWMechanics::CreatureStats::AiSetting(mIndex); ptr.getClass().getCreatureStats (ptr).getAiSetting (mIndex).getBase() + value);
ptr.getClass().getCreatureStats (ptr).setAiSetting (setting,
ptr.getClass().getCreatureStats (ptr).getAiSetting (setting).getBase() + value);
} }
}; };
template<class R> template<class R>
class OpSetAiSetting : public Interpreter::Opcode0 class OpSetAiSetting : public Interpreter::Opcode0
{ {
int mIndex; MWMechanics::CreatureStats::AiSetting mIndex;
public: public:
OpSetAiSetting(int index) : mIndex(index) {} OpSetAiSetting(MWMechanics::CreatureStats::AiSetting index) : mIndex(index) {}
virtual void execute (Interpreter::Runtime& runtime) virtual void execute (Interpreter::Runtime& runtime)
{ {
@ -292,9 +288,7 @@ namespace MWScript
Interpreter::Type_Integer value = runtime[0].mInteger; Interpreter::Type_Integer value = runtime[0].mInteger;
runtime.pop(); runtime.pop();
MWMechanics::CreatureStats::AiSetting setting = (MWMechanics::CreatureStats::AiSetting)mIndex; MWMechanics::Stat<int> stat = ptr.getClass().getCreatureStats(ptr).getAiSetting(mIndex);
MWMechanics::Stat<int> stat = ptr.getClass().getCreatureStats(ptr).getAiSetting(setting);
/* /*
Start of tes3mp addition Start of tes3mp addition
@ -308,7 +302,7 @@ namespace MWScript
*/ */
stat.setModified(value, 0); stat.setModified(value, 0);
ptr.getClass().getCreatureStats(ptr).setAiSetting(setting, stat); ptr.getClass().getCreatureStats(ptr).setAiSetting(mIndex, stat);
/* /*
Start of tes3mp addition Start of tes3mp addition
@ -317,7 +311,7 @@ namespace MWScript
so send a combat packet regardless of whether we're the cell authority or not; the server so send a combat packet regardless of whether we're the cell authority or not; the server
can decide if it wants to comply with them by forwarding them to the cell authority can decide if it wants to comply with them by forwarding them to the cell authority
*/ */
if (stat.getBase() != initialValue && setting == MWMechanics::CreatureStats::AI_Fight && value == 100) if (stat.getBase() != initialValue && mIndex == MWMechanics::CreatureStats::AI_Fight && value == 100)
{ {
mwmp::ActorList *actorList = mwmp::Main::get().getNetworking()->getActorList(); mwmp::ActorList *actorList = mwmp::Main::get().getNetworking()->getActorList();
actorList->reset(); actorList->reset();
@ -638,32 +632,32 @@ namespace MWScript
interpreter.installSegment5 (Compiler::Ai::opcodeStopCombatExplicit, new OpStopCombat<ExplicitRef>); interpreter.installSegment5 (Compiler::Ai::opcodeStopCombatExplicit, new OpStopCombat<ExplicitRef>);
interpreter.installSegment5 (Compiler::Ai::opcodeToggleAI, new OpToggleAI); interpreter.installSegment5 (Compiler::Ai::opcodeToggleAI, new OpToggleAI);
interpreter.installSegment5 (Compiler::Ai::opcodeSetHello, new OpSetAiSetting<ImplicitRef>(0)); interpreter.installSegment5 (Compiler::Ai::opcodeSetHello, new OpSetAiSetting<ImplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Hello));
interpreter.installSegment5 (Compiler::Ai::opcodeSetHelloExplicit, new OpSetAiSetting<ExplicitRef>(0)); interpreter.installSegment5 (Compiler::Ai::opcodeSetHelloExplicit, new OpSetAiSetting<ExplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Hello));
interpreter.installSegment5 (Compiler::Ai::opcodeSetFight, new OpSetAiSetting<ImplicitRef>(1)); interpreter.installSegment5 (Compiler::Ai::opcodeSetFight, new OpSetAiSetting<ImplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Fight));
interpreter.installSegment5 (Compiler::Ai::opcodeSetFightExplicit, new OpSetAiSetting<ExplicitRef>(1)); interpreter.installSegment5 (Compiler::Ai::opcodeSetFightExplicit, new OpSetAiSetting<ExplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Fight));
interpreter.installSegment5 (Compiler::Ai::opcodeSetFlee, new OpSetAiSetting<ImplicitRef>(2)); interpreter.installSegment5 (Compiler::Ai::opcodeSetFlee, new OpSetAiSetting<ImplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Flee));
interpreter.installSegment5 (Compiler::Ai::opcodeSetFleeExplicit, new OpSetAiSetting<ExplicitRef>(2)); interpreter.installSegment5 (Compiler::Ai::opcodeSetFleeExplicit, new OpSetAiSetting<ExplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Flee));
interpreter.installSegment5 (Compiler::Ai::opcodeSetAlarm, new OpSetAiSetting<ImplicitRef>(3)); interpreter.installSegment5 (Compiler::Ai::opcodeSetAlarm, new OpSetAiSetting<ImplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Alarm));
interpreter.installSegment5 (Compiler::Ai::opcodeSetAlarmExplicit, new OpSetAiSetting<ExplicitRef>(3)); interpreter.installSegment5 (Compiler::Ai::opcodeSetAlarmExplicit, new OpSetAiSetting<ExplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Alarm));
interpreter.installSegment5 (Compiler::Ai::opcodeModHello, new OpModAiSetting<ImplicitRef>(0)); interpreter.installSegment5 (Compiler::Ai::opcodeModHello, new OpModAiSetting<ImplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Hello));
interpreter.installSegment5 (Compiler::Ai::opcodeModHelloExplicit, new OpModAiSetting<ExplicitRef>(0)); interpreter.installSegment5 (Compiler::Ai::opcodeModHelloExplicit, new OpModAiSetting<ExplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Hello));
interpreter.installSegment5 (Compiler::Ai::opcodeModFight, new OpModAiSetting<ImplicitRef>(1)); interpreter.installSegment5 (Compiler::Ai::opcodeModFight, new OpModAiSetting<ImplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Fight));
interpreter.installSegment5 (Compiler::Ai::opcodeModFightExplicit, new OpModAiSetting<ExplicitRef>(1)); interpreter.installSegment5 (Compiler::Ai::opcodeModFightExplicit, new OpModAiSetting<ExplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Fight));
interpreter.installSegment5 (Compiler::Ai::opcodeModFlee, new OpModAiSetting<ImplicitRef>(2)); interpreter.installSegment5 (Compiler::Ai::opcodeModFlee, new OpModAiSetting<ImplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Flee));
interpreter.installSegment5 (Compiler::Ai::opcodeModFleeExplicit, new OpModAiSetting<ExplicitRef>(2)); interpreter.installSegment5 (Compiler::Ai::opcodeModFleeExplicit, new OpModAiSetting<ExplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Flee));
interpreter.installSegment5 (Compiler::Ai::opcodeModAlarm, new OpModAiSetting<ImplicitRef>(3)); interpreter.installSegment5 (Compiler::Ai::opcodeModAlarm, new OpModAiSetting<ImplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Alarm));
interpreter.installSegment5 (Compiler::Ai::opcodeModAlarmExplicit, new OpModAiSetting<ExplicitRef>(3)); interpreter.installSegment5 (Compiler::Ai::opcodeModAlarmExplicit, new OpModAiSetting<ExplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Alarm));
interpreter.installSegment5 (Compiler::Ai::opcodeGetHello, new OpGetAiSetting<ImplicitRef>(0)); interpreter.installSegment5 (Compiler::Ai::opcodeGetHello, new OpGetAiSetting<ImplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Hello));
interpreter.installSegment5 (Compiler::Ai::opcodeGetHelloExplicit, new OpGetAiSetting<ExplicitRef>(0)); interpreter.installSegment5 (Compiler::Ai::opcodeGetHelloExplicit, new OpGetAiSetting<ExplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Hello));
interpreter.installSegment5 (Compiler::Ai::opcodeGetFight, new OpGetAiSetting<ImplicitRef>(1)); interpreter.installSegment5 (Compiler::Ai::opcodeGetFight, new OpGetAiSetting<ImplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Fight));
interpreter.installSegment5 (Compiler::Ai::opcodeGetFightExplicit, new OpGetAiSetting<ExplicitRef>(1)); interpreter.installSegment5 (Compiler::Ai::opcodeGetFightExplicit, new OpGetAiSetting<ExplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Fight));
interpreter.installSegment5 (Compiler::Ai::opcodeGetFlee, new OpGetAiSetting<ImplicitRef>(2)); interpreter.installSegment5 (Compiler::Ai::opcodeGetFlee, new OpGetAiSetting<ImplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Flee));
interpreter.installSegment5 (Compiler::Ai::opcodeGetFleeExplicit, new OpGetAiSetting<ExplicitRef>(2)); interpreter.installSegment5 (Compiler::Ai::opcodeGetFleeExplicit, new OpGetAiSetting<ExplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Flee));
interpreter.installSegment5 (Compiler::Ai::opcodeGetAlarm, new OpGetAiSetting<ImplicitRef>(3)); interpreter.installSegment5 (Compiler::Ai::opcodeGetAlarm, new OpGetAiSetting<ImplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Alarm));
interpreter.installSegment5 (Compiler::Ai::opcodeGetAlarmExplicit, new OpGetAiSetting<ExplicitRef>(3)); interpreter.installSegment5 (Compiler::Ai::opcodeGetAlarmExplicit, new OpGetAiSetting<ExplicitRef>(MWMechanics::CreatureStats::AiSetting::AI_Alarm));
interpreter.installSegment5 (Compiler::Ai::opcodeFace, new OpFace<ImplicitRef>); interpreter.installSegment5 (Compiler::Ai::opcodeFace, new OpFace<ImplicitRef>);
interpreter.installSegment5 (Compiler::Ai::opcodeFaceExplicit, new OpFace<ExplicitRef>); interpreter.installSegment5 (Compiler::Ai::opcodeFaceExplicit, new OpFace<ExplicitRef>);

View file

@ -455,7 +455,6 @@ namespace MWScript
} }
}; };
template<class R>
class OpPlaceItemCell : public Interpreter::Opcode0 class OpPlaceItemCell : public Interpreter::Opcode0
{ {
public: public:
@ -547,7 +546,6 @@ namespace MWScript
} }
}; };
template<class R>
class OpPlaceItem : public Interpreter::Opcode0 class OpPlaceItem : public Interpreter::Opcode0
{ {
public: public:
@ -922,8 +920,8 @@ namespace MWScript
interpreter.installSegment5(Compiler::Transformation::opcodePositionExplicit,new OpPosition<ExplicitRef>); interpreter.installSegment5(Compiler::Transformation::opcodePositionExplicit,new OpPosition<ExplicitRef>);
interpreter.installSegment5(Compiler::Transformation::opcodePositionCell,new OpPositionCell<ImplicitRef>); interpreter.installSegment5(Compiler::Transformation::opcodePositionCell,new OpPositionCell<ImplicitRef>);
interpreter.installSegment5(Compiler::Transformation::opcodePositionCellExplicit,new OpPositionCell<ExplicitRef>); interpreter.installSegment5(Compiler::Transformation::opcodePositionCellExplicit,new OpPositionCell<ExplicitRef>);
interpreter.installSegment5(Compiler::Transformation::opcodePlaceItemCell,new OpPlaceItemCell<ImplicitRef>); interpreter.installSegment5(Compiler::Transformation::opcodePlaceItemCell,new OpPlaceItemCell);
interpreter.installSegment5(Compiler::Transformation::opcodePlaceItem,new OpPlaceItem<ImplicitRef>); interpreter.installSegment5(Compiler::Transformation::opcodePlaceItem,new OpPlaceItem);
interpreter.installSegment5(Compiler::Transformation::opcodePlaceAtPc,new OpPlaceAt<ImplicitRef, true>); interpreter.installSegment5(Compiler::Transformation::opcodePlaceAtPc,new OpPlaceAt<ImplicitRef, true>);
interpreter.installSegment5(Compiler::Transformation::opcodePlaceAtMe,new OpPlaceAt<ImplicitRef, false>); interpreter.installSegment5(Compiler::Transformation::opcodePlaceAtMe,new OpPlaceAt<ImplicitRef, false>);
interpreter.installSegment5(Compiler::Transformation::opcodePlaceAtMeExplicit,new OpPlaceAt<ExplicitRef, false>); interpreter.installSegment5(Compiler::Transformation::opcodePlaceAtMeExplicit,new OpPlaceAt<ExplicitRef, false>);

View file

@ -26,7 +26,8 @@
#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/mechanicsmanager.hpp"
#include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/npcstats.hpp"
#include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/spellresistance.hpp"
#include "../mwmechanics/spellutil.hpp"
#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/actorutil.hpp"
#include "../mwmechanics/weapontype.hpp" #include "../mwmechanics/weapontype.hpp"

View file

@ -32,7 +32,7 @@
#include "../mwmechanics/movement.hpp" #include "../mwmechanics/movement.hpp"
#include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/npcstats.hpp"
#include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/spellutil.hpp"
#include "class.hpp" #include "class.hpp"
#include "ptr.hpp" #include "ptr.hpp"

View file

@ -63,6 +63,7 @@
#include "../mwmechanics/levelledlist.hpp" #include "../mwmechanics/levelledlist.hpp"
#include "../mwmechanics/combat.hpp" #include "../mwmechanics/combat.hpp"
#include "../mwmechanics/aiavoiddoor.hpp" //Used to tell actors to avoid doors #include "../mwmechanics/aiavoiddoor.hpp" //Used to tell actors to avoid doors
#include "../mwmechanics/summoning.hpp"
#include "../mwrender/animation.hpp" #include "../mwrender/animation.hpp"
#include "../mwrender/npcanimation.hpp" #include "../mwrender/npcanimation.hpp"
@ -3705,12 +3706,42 @@ namespace MWWorld
mProjectileManager->launchMagicBolt(spellId, caster, fallbackDirection); mProjectileManager->launchMagicBolt(spellId, caster, fallbackDirection);
} }
class ApplyLoopingParticlesVisitor : public MWMechanics::EffectSourceVisitor
{
private:
MWWorld::Ptr mActor;
public:
ApplyLoopingParticlesVisitor(const MWWorld::Ptr& actor)
: mActor(actor)
{
}
virtual void visit (MWMechanics::EffectKey key,
const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/,
float /*magnitude*/, float /*remainingTime*/ = -1, float /*totalTime*/ = -1)
{
const ESMStore& store = MWBase::Environment::get().getWorld()->getStore();
const auto magicEffect = store.get<ESM::MagicEffect>().find(key.mId);
if ((magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) == 0)
return;
const ESM::Static* castStatic;
if (!magicEffect->mHit.empty())
castStatic = store.get<ESM::Static>().find (magicEffect->mHit);
else
castStatic = store.get<ESM::Static>().find ("VFX_DefaultHit");
MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mActor);
if (anim && !castStatic->mModel.empty())
anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, /*loop*/true, "", magicEffect->mParticle);
}
};
void World::applyLoopingParticles(const MWWorld::Ptr& ptr) void World::applyLoopingParticles(const MWWorld::Ptr& ptr)
{ {
const MWWorld::Class &cls = ptr.getClass(); const MWWorld::Class &cls = ptr.getClass();
if (cls.isActor()) if (cls.isActor())
{ {
MWMechanics::ApplyLoopingParticlesVisitor visitor(ptr); ApplyLoopingParticlesVisitor visitor(ptr);
cls.getCreatureStats(ptr).getActiveSpells().visitEffectSources(visitor); cls.getCreatureStats(ptr).getActiveSpells().visitEffectSources(visitor);
cls.getCreatureStats(ptr).getSpells().visitEffectSources(visitor); cls.getCreatureStats(ptr).getSpells().visitEffectSources(visitor);
if (cls.hasInventoryStore(ptr)) if (cls.hasInventoryStore(ptr))

View file

@ -27,6 +27,10 @@ if (GTEST_FOUND AND GMOCK_FOUND)
detournavigator/tilecachedrecastmeshmanager.cpp detournavigator/tilecachedrecastmeshmanager.cpp
settings/parser.cpp settings/parser.cpp
shader/parsedefines.cpp
shader/parsefors.cpp
shader/shadermanager.cpp
) )
source_group(apps\\openmw_test_suite FILES openmw_test_suite.cpp ${UNITTEST_SRC_FILES}) source_group(apps\\openmw_test_suite FILES openmw_test_suite.cpp ${UNITTEST_SRC_FILES})

View file

@ -73,6 +73,7 @@ namespace
mSettings.mTrianglesPerChunk = 256; mSettings.mTrianglesPerChunk = 256;
mSettings.mMaxPolys = 4096; mSettings.mMaxPolys = 4096;
mSettings.mMaxTilesNumber = 512; mSettings.mMaxTilesNumber = 512;
mSettings.mMinUpdateInterval = std::chrono::milliseconds(50);
mNavigator.reset(new NavigatorImpl(mSettings)); mNavigator.reset(new NavigatorImpl(mSettings));
} }
}; };
@ -699,4 +700,108 @@ namespace
EXPECT_FLOAT_EQ(distance, 85.260780334472656); EXPECT_FLOAT_EQ(distance, 85.260780334472656);
} }
TEST_F(DetourNavigatorNavigatorTest, multiple_threads_should_lock_tiles)
{
mSettings.mAsyncNavMeshUpdaterThreads = 2;
mNavigator.reset(new NavigatorImpl(mSettings));
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));
const std::vector<btBoxShape> boxShapes(100, btVector3(20, 20, 100));
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addObject(ObjectId(&heightfieldShape), heightfieldShape, btTransform::getIdentity());
for (std::size_t i = 0; i < boxShapes.size(); ++i)
{
const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 10, i * 10, i * 10));
mNavigator->addObject(ObjectId(&boxShapes[i]), boxShapes[i], transform);
}
std::this_thread::sleep_for(std::chrono::microseconds(1));
for (std::size_t i = 0; i < boxShapes.size(); ++i)
{
const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 10 + 1, i * 10 + 1, i * 10 + 1));
mNavigator->updateObject(ObjectId(&boxShapes[i]), boxShapes[i], transform);
}
mNavigator->update(mPlayerPosition);
mNavigator->wait();
EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mOut), Status::Success);
EXPECT_THAT(mPath, ElementsAre(
Vec3fEq(-215, 215, 1.8782780170440673828125),
Vec3fEq(-199.7968292236328125, 191.09100341796875, -3.54875946044921875),
Vec3fEq(-184.5936431884765625, 167.1819915771484375, -8.97846889495849609375),
Vec3fEq(-169.3904571533203125, 143.2729949951171875, -14.40818119049072265625),
Vec3fEq(-154.1872711181640625, 119.363983154296875, -19.837886810302734375),
Vec3fEq(-138.9840850830078125, 95.4549713134765625, -25.2675952911376953125),
Vec3fEq(-123.78090667724609375, 71.54595947265625, -30.6973056793212890625),
Vec3fEq(-108.57772064208984375, 47.63695526123046875, -36.12701416015625),
Vec3fEq(-93.3745269775390625, 23.72794342041015625, -40.754695892333984375),
Vec3fEq(-78.17134857177734375, -0.18106450140476226806640625, -37.128795623779296875),
Vec3fEq(-62.968158721923828125, -24.0900726318359375, -33.50289154052734375),
Vec3fEq(-47.764972686767578125, -47.99908447265625, -30.797946929931640625),
Vec3fEq(-23.8524494171142578125, -63.196746826171875, -33.97112274169921875),
Vec3fEq(0.0600722394883632659912109375, -78.3944091796875, -37.14543914794921875),
Vec3fEq(23.97259521484375, -93.592071533203125, -40.774089813232421875),
Vec3fEq(47.885120391845703125, -108.78974151611328125, -36.051296234130859375),
Vec3fEq(71.797637939453125, -123.98740386962890625, -30.62355804443359375),
Vec3fEq(95.71016693115234375, -139.18505859375, -25.195819854736328125),
Vec3fEq(119.6226806640625, -154.382720947265625, -19.768085479736328125),
Vec3fEq(143.5352020263671875, -169.5803680419921875, -14.34035015106201171875),
Vec3fEq(167.447723388671875, -184.7780303955078125, -8.912616729736328125),
Vec3fEq(191.3602294921875, -199.9756927490234375, -3.48488140106201171875),
Vec3fEq(215, -215, 1.8782813549041748046875)
)) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, update_changed_multiple_times_object_should_delay_navmesh_change)
{
const std::vector<btBoxShape> shapes(100, btVector3(64, 64, 64));
mNavigator->addAgent(mAgentHalfExtents);
for (std::size_t i = 0; i < shapes.size(); ++i)
{
const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 32, i * 32, i * 32));
mNavigator->addObject(ObjectId(&shapes[i]), shapes[i], transform);
}
mNavigator->update(mPlayerPosition);
mNavigator->wait();
const auto start = std::chrono::steady_clock::now();
for (std::size_t i = 0; i < shapes.size(); ++i)
{
const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 32 + 1, i * 32 + 1, i * 32 + 1));
mNavigator->updateObject(ObjectId(&shapes[i]), shapes[i], transform);
}
mNavigator->update(mPlayerPosition);
mNavigator->wait();
for (std::size_t i = 0; i < shapes.size(); ++i)
{
const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 32 + 2, i * 32 + 2, i * 32 + 2));
mNavigator->updateObject(ObjectId(&shapes[i]), shapes[i], transform);
}
mNavigator->update(mPlayerPosition);
mNavigator->wait();
const auto duration = std::chrono::steady_clock::now() - start;
EXPECT_GT(duration, mSettings.mMinUpdateInterval)
<< std::chrono::duration_cast<std::chrono::duration<float, std::milli>>(duration).count() << " ms";
}
} }

View file

@ -20,6 +20,7 @@ namespace
struct DetourNavigatorTileCachedRecastMeshManagerTest : Test struct DetourNavigatorTileCachedRecastMeshManagerTest : Test
{ {
Settings mSettings; Settings mSettings;
std::vector<TilePosition> mChangedTiles;
DetourNavigatorTileCachedRecastMeshManagerTest() DetourNavigatorTileCachedRecastMeshManagerTest()
{ {
@ -29,6 +30,11 @@ namespace
mSettings.mTileSize = 64; mSettings.mTileSize = 64;
mSettings.mTrianglesPerChunk = 256; mSettings.mTrianglesPerChunk = 256;
} }
void onChangedTile(const TilePosition& tilePosition)
{
mChangedTiles.push_back(tilePosition);
}
}; };
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_for_empty_should_return_nullptr) TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_for_empty_should_return_nullptr)
@ -78,8 +84,10 @@ namespace
const btBoxShape boxShape(btVector3(20, 20, 100)); const btBoxShape boxShape(btVector3(20, 20, 100));
const btTransform transform(btMatrix3x3::getIdentity(), btVector3(getTileSize(mSettings) / mSettings.mRecastScaleFactor, 0, 0)); const btTransform transform(btMatrix3x3::getIdentity(), btVector3(getTileSize(mSettings) / mSettings.mRecastScaleFactor, 0, 0));
manager.addObject(ObjectId(&boxShape), boxShape, transform, AreaType::AreaType_ground); manager.addObject(ObjectId(&boxShape), boxShape, transform, AreaType::AreaType_ground);
EXPECT_TRUE(manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground,
[&] (const auto& v) { onChangedTile(v); }));
EXPECT_THAT( EXPECT_THAT(
manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground), mChangedTiles,
ElementsAre(TilePosition(-1, -1), TilePosition(-1, 0), TilePosition(0, -1), TilePosition(0, 0), ElementsAre(TilePosition(-1, -1), TilePosition(-1, 0), TilePosition(0, -1), TilePosition(0, 0),
TilePosition(1, -1), TilePosition(1, 0)) TilePosition(1, -1), TilePosition(1, 0))
); );
@ -90,10 +98,9 @@ namespace
TileCachedRecastMeshManager manager(mSettings); TileCachedRecastMeshManager manager(mSettings);
const btBoxShape boxShape(btVector3(20, 20, 100)); const btBoxShape boxShape(btVector3(20, 20, 100));
manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground);
EXPECT_EQ( EXPECT_FALSE(manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground,
manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground), [&] (const auto& v) { onChangedTile(v); }));
std::vector<TilePosition>() EXPECT_EQ(mChangedTiles, std::vector<TilePosition>());
);
} }
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_after_add_object_should_return_recast_mesh_for_each_used_tile) TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_after_add_object_should_return_recast_mesh_for_each_used_tile)
@ -127,7 +134,7 @@ namespace
EXPECT_NE(manager.getMesh(TilePosition(1, 0)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(1, 0)), nullptr);
EXPECT_NE(manager.getMesh(TilePosition(1, -1)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(1, -1)), nullptr);
manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {});
EXPECT_NE(manager.getMesh(TilePosition(-1, -1)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(-1, -1)), nullptr);
EXPECT_NE(manager.getMesh(TilePosition(-1, 0)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(-1, 0)), nullptr);
EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr);
@ -144,7 +151,7 @@ namespace
EXPECT_EQ(manager.getMesh(TilePosition(-1, -1)), nullptr); EXPECT_EQ(manager.getMesh(TilePosition(-1, -1)), nullptr);
EXPECT_EQ(manager.getMesh(TilePosition(-1, 0)), nullptr); EXPECT_EQ(manager.getMesh(TilePosition(-1, 0)), nullptr);
manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {});
EXPECT_EQ(manager.getMesh(TilePosition(1, 0)), nullptr); EXPECT_EQ(manager.getMesh(TilePosition(1, 0)), nullptr);
EXPECT_EQ(manager.getMesh(TilePosition(1, -1)), nullptr); EXPECT_EQ(manager.getMesh(TilePosition(1, -1)), nullptr);
} }
@ -172,7 +179,7 @@ namespace
EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr);
EXPECT_NE(manager.getMesh(TilePosition(0, 0)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(0, 0)), nullptr);
manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {});
EXPECT_NE(manager.getMesh(TilePosition(-1, -1)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(-1, -1)), nullptr);
EXPECT_NE(manager.getMesh(TilePosition(-1, 0)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(-1, 0)), nullptr);
EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr);
@ -205,7 +212,7 @@ namespace
const btTransform transform(btMatrix3x3::getIdentity(), btVector3(getTileSize(mSettings) / mSettings.mRecastScaleFactor, 0, 0)); const btTransform transform(btMatrix3x3::getIdentity(), btVector3(getTileSize(mSettings) / mSettings.mRecastScaleFactor, 0, 0));
manager.addObject(ObjectId(&boxShape), boxShape, transform, AreaType::AreaType_ground); manager.addObject(ObjectId(&boxShape), boxShape, transform, AreaType::AreaType_ground);
const auto beforeUpdateRevision = manager.getRevision(); const auto beforeUpdateRevision = manager.getRevision();
manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {});
EXPECT_EQ(manager.getRevision(), beforeUpdateRevision + 1); EXPECT_EQ(manager.getRevision(), beforeUpdateRevision + 1);
} }
@ -215,7 +222,7 @@ namespace
const btBoxShape boxShape(btVector3(20, 20, 100)); const btBoxShape boxShape(btVector3(20, 20, 100));
manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground);
const auto beforeUpdateRevision = manager.getRevision(); const auto beforeUpdateRevision = manager.getRevision();
manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {});
EXPECT_EQ(manager.getRevision(), beforeUpdateRevision); EXPECT_EQ(manager.getRevision(), beforeUpdateRevision);
} }

View file

@ -0,0 +1,191 @@
#include <components/shader/shadermanager.hpp>
#include <gtest/gtest.h>
#include <gmock/gmock.h>
namespace
{
using namespace testing;
using namespace Shader;
using DefineMap = ShaderManager::DefineMap;
struct ShaderParseDefinesTest : Test
{
std::string mSource;
const std::string mName = "shader";
DefineMap mDefines;
DefineMap mGlobalDefines;
};
TEST_F(ShaderParseDefinesTest, empty_should_succeed)
{
ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "");
}
TEST_F(ShaderParseDefinesTest, should_fail_for_absent_define)
{
mSource = "@foo\n";
ASSERT_FALSE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "@foo\n");
}
TEST_F(ShaderParseDefinesTest, should_replace_by_existing_define)
{
mDefines["foo"] = "42";
mSource = "@foo\n";
ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "42\n");
}
TEST_F(ShaderParseDefinesTest, should_replace_by_existing_global_define)
{
mGlobalDefines["foo"] = "42";
mSource = "@foo\n";
ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "42\n");
}
TEST_F(ShaderParseDefinesTest, should_prefer_define_over_global_define)
{
mDefines["foo"] = "13";
mGlobalDefines["foo"] = "42";
mSource = "@foo\n";
ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "13\n");
}
namespace SupportedTerminals
{
struct ShaderParseDefinesTest : ::ShaderParseDefinesTest, WithParamInterface<char> {};
TEST_P(ShaderParseDefinesTest, support_defines_terminated_by)
{
mDefines["foo"] = "13";
mSource = "@foo" + std::string(1, GetParam());
ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "13" + std::string(1, GetParam()));
}
INSTANTIATE_TEST_SUITE_P(
SupportedTerminals,
ShaderParseDefinesTest,
Values(' ', '\n', '\r', '(', ')', '[', ']', '.', ';', ',')
);
}
TEST_F(ShaderParseDefinesTest, should_not_support_define_ending_with_source)
{
mDefines["foo"] = "42";
mSource = "@foo";
ASSERT_FALSE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "@foo");
}
TEST_F(ShaderParseDefinesTest, should_replace_all_matched_values)
{
mDefines["foo"] = "42";
mSource = "@foo @foo ";
ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "42 42 ");
}
TEST_F(ShaderParseDefinesTest, should_support_define_with_empty_name)
{
mDefines[""] = "42";
mSource = "@ ";
ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "42 ");
}
TEST_F(ShaderParseDefinesTest, should_replace_all_found_defines)
{
mDefines["foo"] = "42";
mDefines["bar"] = "13";
mDefines["baz"] = "55";
mSource = "@foo @bar ";
ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "42 13 ");
}
TEST_F(ShaderParseDefinesTest, should_fail_on_foreach_without_endforeach)
{
mSource = "@foreach ";
ASSERT_FALSE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "$foreach ");
}
TEST_F(ShaderParseDefinesTest, should_fail_on_endforeach_without_foreach)
{
mSource = "@endforeach ";
ASSERT_FALSE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "$endforeach ");
}
TEST_F(ShaderParseDefinesTest, should_replace_at_sign_by_dollar_for_foreach_endforeach)
{
mSource = "@foreach @endforeach ";
ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "$foreach $endforeach ");
}
TEST_F(ShaderParseDefinesTest, should_succeed_on_unmatched_nested_foreach)
{
mSource = "@foreach @foreach @endforeach ";
ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "$foreach $foreach $endforeach ");
}
TEST_F(ShaderParseDefinesTest, should_fail_on_unmatched_nested_endforeach)
{
mSource = "@foreach @endforeach @endforeach ";
ASSERT_FALSE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "$foreach $endforeach $endforeach ");
}
TEST_F(ShaderParseDefinesTest, should_support_nested_foreach)
{
mSource = "@foreach @foreach @endforeach @endforeach ";
ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "$foreach $foreach $endforeach $endforeach ");
}
TEST_F(ShaderParseDefinesTest, should_support_foreach_variable)
{
mSource = "@foreach foo @foo @endforeach ";
ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "$foreach foo $foo $endforeach ");
}
TEST_F(ShaderParseDefinesTest, should_not_replace_foreach_variable_by_define)
{
mDefines["foo"] = "42";
mSource = "@foreach foo @foo @endforeach ";
ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "$foreach foo $foo $endforeach ");
}
TEST_F(ShaderParseDefinesTest, should_support_nested_foreach_with_variable)
{
mSource = "@foreach foo @foo @foreach bar @bar @endforeach @endforeach ";
ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "$foreach foo $foo $foreach bar $bar $endforeach $endforeach ");
}
TEST_F(ShaderParseDefinesTest, should_not_support_single_line_comments_for_defines)
{
mDefines["foo"] = "42";
mSource = "@foo // @foo\n";
ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "42 // 42\n");
}
TEST_F(ShaderParseDefinesTest, should_not_support_multiline_comments_for_defines)
{
mDefines["foo"] = "42";
mSource = "/* @foo */ @foo ";
ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName));
EXPECT_EQ(mSource, "/* 42 */ 42 ");
}
}

View file

@ -0,0 +1,94 @@
#include <components/shader/shadermanager.hpp>
#include <gtest/gtest.h>
#include <gmock/gmock.h>
namespace
{
using namespace testing;
using namespace Shader;
using DefineMap = ShaderManager::DefineMap;
struct ShaderParseForsTest : Test
{
std::string mSource;
const std::string mName = "shader";
};
TEST_F(ShaderParseForsTest, empty_should_succeed)
{
ASSERT_TRUE(parseFors(mSource, mName));
EXPECT_EQ(mSource, "");
}
TEST_F(ShaderParseForsTest, should_fail_for_single_escape_symbol)
{
mSource = "$";
ASSERT_FALSE(parseFors(mSource, mName));
EXPECT_EQ(mSource, "$");
}
TEST_F(ShaderParseForsTest, should_fail_on_first_found_escaped_not_foreach)
{
mSource = "$foo ";
ASSERT_FALSE(parseFors(mSource, mName));
EXPECT_EQ(mSource, "$foo ");
}
TEST_F(ShaderParseForsTest, should_fail_on_absent_foreach_variable)
{
mSource = "$foreach ";
ASSERT_FALSE(parseFors(mSource, mName));
EXPECT_EQ(mSource, "$foreach ");
}
TEST_F(ShaderParseForsTest, should_fail_on_unmatched_after_variable)
{
mSource = "$foreach foo ";
ASSERT_FALSE(parseFors(mSource, mName));
EXPECT_EQ(mSource, "$foreach foo ");
}
TEST_F(ShaderParseForsTest, should_fail_on_absent_newline_after_foreach_list)
{
mSource = "$foreach foo 1,2,3 ";
ASSERT_FALSE(parseFors(mSource, mName));
EXPECT_EQ(mSource, "$foreach foo 1,2,3 ");
}
TEST_F(ShaderParseForsTest, should_fail_on_absent_endforeach_after_newline)
{
mSource = "$foreach foo 1,2,3\n";
ASSERT_FALSE(parseFors(mSource, mName));
EXPECT_EQ(mSource, "$foreach foo 1,2,3\n");
}
TEST_F(ShaderParseForsTest, should_replace_complete_foreach_by_line_number)
{
mSource = "$foreach foo 1,2,3\n$endforeach";
ASSERT_TRUE(parseFors(mSource, mName));
EXPECT_EQ(mSource, "\n#line 3");
}
TEST_F(ShaderParseForsTest, should_replace_loop_variable)
{
mSource = "$foreach foo 1,2,3\n$foo\n$endforeach";
ASSERT_TRUE(parseFors(mSource, mName));
EXPECT_EQ(mSource, "1\n2\n3\n\n#line 4");
}
TEST_F(ShaderParseForsTest, should_count_line_number_from_existing)
{
mSource = "$foreach foo 1,2,3\n#line 10\n$foo\n$endforeach";
ASSERT_TRUE(parseFors(mSource, mName));
EXPECT_EQ(mSource, "#line 10\n1\n#line 10\n2\n#line 10\n3\n\n#line 12");
}
TEST_F(ShaderParseForsTest, should_not_support_nested_loops)
{
mSource = "$foreach foo 1,2\n$foo\n$foreach bar 1,2\n$bar\n$endforeach\n$endforeach";
ASSERT_FALSE(parseFors(mSource, mName));
EXPECT_EQ(mSource, "1\n1\n2\n$foreach bar 1,2\n1\n\n#line 6\n2\n2\n$foreach bar 1,2\n2\n\n#line 6\n\n#line 7");
}
}

View file

@ -0,0 +1,240 @@
#include <components/shader/shadermanager.hpp>
#include <boost/filesystem/fstream.hpp>
#include <gtest/gtest.h>
namespace
{
using namespace testing;
using namespace Shader;
struct ShaderManagerTest : Test
{
ShaderManager mManager;
ShaderManager::DefineMap mDefines;
ShaderManagerTest()
{
mManager.setShaderPath(".");
}
template <class F>
void withShaderFile(const std::string& content, F&& f)
{
withShaderFile("", content, std::forward<F>(f));
}
template <class F>
void withShaderFile(const std::string& suffix, const std::string& content, F&& f)
{
const auto path = UnitTest::GetInstance()->current_test_info()->name() + suffix + ".glsl";
{
boost::filesystem::ofstream stream;
stream.open(path);
stream << content;
stream.close();
}
f(path);
}
};
TEST_F(ShaderManagerTest, get_shader_with_empty_content_should_succeed)
{
const std::string content;
withShaderFile(content, [this] (const std::string& templateName) {
EXPECT_TRUE(mManager.getShader(templateName, {}, osg::Shader::VERTEX));
});
}
TEST_F(ShaderManagerTest, get_shader_should_not_change_source_without_template_parameters)
{
const std::string content =
"#version 120\n"
"void main() {}\n";
withShaderFile(content, [&] (const std::string& templateName) {
const auto shader = mManager.getShader(templateName, mDefines, osg::Shader::VERTEX);
ASSERT_TRUE(shader);
EXPECT_EQ(shader->getShaderSource(), content);
});
}
TEST_F(ShaderManagerTest, get_shader_should_replace_includes_with_content)
{
const std::string content0 =
"void foo() {}\n";
withShaderFile("_0", content0, [&] (const std::string& templateName0) {
const std::string content1 =
"#include \"" + templateName0 + "\"\n"
"void bar() { foo() }\n";
withShaderFile("_1", content1, [&] (const std::string& templateName1) {
const std::string content2 =
"#version 120\n"
"#include \"" + templateName1 + "\"\n"
"void main() { bar() }\n";
withShaderFile(content2, [&] (const std::string& templateName2) {
const auto shader = mManager.getShader(templateName2, mDefines, osg::Shader::VERTEX);
ASSERT_TRUE(shader);
const std::string expected =
"#version 120\n"
"#line 0 1\n"
"#line 0 2\n"
"void foo() {}\n"
"\n"
"#line 0 0\n"
"\n"
"void bar() { foo() }\n"
"\n"
"#line 2 0\n"
"\n"
"void main() { bar() }\n";
EXPECT_EQ(shader->getShaderSource(), expected);
});
});
});
}
TEST_F(ShaderManagerTest, get_shader_should_replace_defines)
{
const std::string content =
"#version 120\n"
"#define FLAG @flag\n"
"void main() {}\n"
;
withShaderFile(content, [&] (const std::string& templateName) {
mDefines["flag"] = "1";
const auto shader = mManager.getShader(templateName, mDefines, osg::Shader::VERTEX);
ASSERT_TRUE(shader);
const std::string expected =
"#version 120\n"
"#define FLAG 1\n"
"void main() {}\n";
EXPECT_EQ(shader->getShaderSource(), expected);
});
}
TEST_F(ShaderManagerTest, get_shader_should_expand_loop)
{
const std::string content =
"#version 120\n"
"@foreach index @list\n"
" varying vec4 foo@index;\n"
"@endforeach\n"
"void main() {}\n"
;
withShaderFile(content, [&] (const std::string& templateName) {
mDefines["list"] = "1,2,3";
const auto shader = mManager.getShader(templateName, mDefines, osg::Shader::VERTEX);
ASSERT_TRUE(shader);
const std::string expected =
"#version 120\n"
" varying vec4 foo1;\n"
" varying vec4 foo2;\n"
" varying vec4 foo3;\n"
"\n"
"#line 5\n"
"void main() {}\n";
EXPECT_EQ(shader->getShaderSource(), expected);
});
}
TEST_F(ShaderManagerTest, get_shader_should_replace_loops_with_conditions)
{
const std::string content =
"#version 120\n"
"@foreach index @list\n"
" varying vec4 foo@index;\n"
"@endforeach\n"
"void main()\n"
"{\n"
"#ifdef BAR\n"
"@foreach index @list\n"
" foo@index = vec4(1.0);\n"
"@endforeach\n"
"#elif BAZ\n"
"@foreach index @list\n"
" foo@index = vec4(2.0);\n"
"@endforeach\n"
"#else\n"
"@foreach index @list\n"
" foo@index = vec4(3.0);\n"
"@endforeach\n"
"#endif\n"
"}\n"
;
withShaderFile(content, [&] (const std::string& templateName) {
mDefines["list"] = "1,2,3";
const auto shader = mManager.getShader(templateName, mDefines, osg::Shader::VERTEX);
ASSERT_TRUE(shader);
const std::string expected =
"#version 120\n"
" varying vec4 foo1;\n"
" varying vec4 foo2;\n"
" varying vec4 foo3;\n"
"\n"
"#line 5\n"
"void main()\n"
"{\n"
"#ifdef BAR\n"
" foo1 = vec4(1.0);\n"
" foo2 = vec4(1.0);\n"
" foo3 = vec4(1.0);\n"
"\n"
"#line 11\n"
"#elif BAZ\n"
"#line 12\n"
" foo1 = vec4(2.0);\n"
" foo2 = vec4(2.0);\n"
" foo3 = vec4(2.0);\n"
"\n"
"#line 15\n"
"#else\n"
"#line 16\n"
" foo1 = vec4(3.0);\n"
" foo2 = vec4(3.0);\n"
" foo3 = vec4(3.0);\n"
"\n"
"#line 19\n"
"#endif\n"
"#line 20\n"
"}\n";
EXPECT_EQ(shader->getShaderSource(), expected);
});
}
TEST_F(ShaderManagerTest, get_shader_should_fail_on_absent_template_parameters_in_single_line_comments)
{
const std::string content =
"#version 120\n"
"// #define FLAG @flag\n"
"void main() {}\n"
;
withShaderFile(content, [&] (const std::string& templateName) {
EXPECT_FALSE(mManager.getShader(templateName, mDefines, osg::Shader::VERTEX));
});
}
TEST_F(ShaderManagerTest, get_shader_should_fail_on_absent_template_parameter_in_multi_line_comments)
{
const std::string content =
"#version 120\n"
"/* #define FLAG @flag */\n"
"void main() {}\n"
;
withShaderFile(content, [&] (const std::string& templateName) {
EXPECT_FALSE(mManager.getShader(templateName, mDefines, osg::Shader::VERTEX));
});
}
}

View file

@ -555,6 +555,9 @@ static bool is_debugger_present()
void crashCatcherInstall(int argc, char **argv, const std::string &crashLogPath) void crashCatcherInstall(int argc, char **argv, const std::string &crashLogPath)
{ {
if (const auto env = std::getenv("OPENMW_DISABLE_CRASH_CATCHER"))
if (std::atol(env) != 0)
return;
if ((argc == 2 && strcmp(argv[1], crash_switch) == 0) || !is_debugger_present()) if ((argc == 2 && strcmp(argv[1], crash_switch) == 0) || !is_debugger_present())
{ {
int s[5] = { SIGSEGV, SIGILL, SIGFPE, SIGBUS, SIGABRT }; int s[5] = { SIGSEGV, SIGILL, SIGFPE, SIGBUS, SIGABRT };

View file

@ -7,6 +7,8 @@
#include <osg/Stats> #include <osg/Stats>
#include <numeric>
namespace namespace
{ {
using DetourNavigator::ChangeType; using DetourNavigator::ChangeType;
@ -87,6 +89,9 @@ namespace DetourNavigator
job.mChangeType = changedTile.second; job.mChangeType = changedTile.second;
job.mDistanceToPlayer = getManhattanDistance(changedTile.first, playerTile); job.mDistanceToPlayer = getManhattanDistance(changedTile.first, playerTile);
job.mDistanceToOrigin = getManhattanDistance(changedTile.first, TilePosition {0, 0}); job.mDistanceToOrigin = getManhattanDistance(changedTile.first, TilePosition {0, 0});
job.mProcessTime = job.mChangeType == ChangeType::update
? mLastUpdates[job.mAgentHalfExtents][job.mChangedTile] + mSettings.get().mMinUpdateInterval
: std::chrono::steady_clock::time_point();
mJobs.push(std::move(job)); mJobs.push(std::move(job));
} }
@ -100,8 +105,11 @@ namespace DetourNavigator
void AsyncNavMeshUpdater::wait() void AsyncNavMeshUpdater::wait()
{ {
std::unique_lock<std::mutex> lock(mMutex); {
mDone.wait(lock, [&] { return mJobs.empty(); }); std::unique_lock<std::mutex> lock(mMutex);
mDone.wait(lock, [&] { return mJobs.empty() && getTotalThreadJobsUnsafe() == 0; });
}
mProcessingTiles.wait(mProcessed, [] (const auto& v) { return v.empty(); });
} }
void AsyncNavMeshUpdater::reportStats(unsigned int frameNumber, osg::Stats& stats) const void AsyncNavMeshUpdater::reportStats(unsigned int frameNumber, osg::Stats& stats) const
@ -110,7 +118,7 @@ namespace DetourNavigator
{ {
const std::lock_guard<std::mutex> lock(mMutex); const std::lock_guard<std::mutex> lock(mMutex);
jobs = mJobs.size(); jobs = mJobs.size() + getTotalThreadJobsUnsafe();
} }
stats.setAttribute(frameNumber, "NavMesh UpdateJobs", jobs); stats.setAttribute(frameNumber, "NavMesh UpdateJobs", jobs);
@ -120,7 +128,7 @@ namespace DetourNavigator
void AsyncNavMeshUpdater::process() throw() void AsyncNavMeshUpdater::process() throw()
{ {
Log(Debug::Debug) << "Start process navigator jobs"; Log(Debug::Debug) << "Start process navigator jobs by thread=" << std::this_thread::get_id();
while (!mShouldStop) while (!mShouldStop)
{ {
try try
@ -132,18 +140,21 @@ namespace DetourNavigator
if (!processed) if (!processed)
repost(std::move(*job)); repost(std::move(*job));
} }
else
cleanupLastUpdates();
} }
catch (const std::exception& e) catch (const std::exception& e)
{ {
Log(Debug::Error) << "AsyncNavMeshUpdater::process exception: " << e.what(); Log(Debug::Error) << "AsyncNavMeshUpdater::process exception: " << e.what();
} }
} }
Log(Debug::Debug) << "Stop navigator jobs processing"; Log(Debug::Debug) << "Stop navigator jobs processing by thread=" << std::this_thread::get_id();
} }
bool AsyncNavMeshUpdater::processJob(const Job& job) bool AsyncNavMeshUpdater::processJob(const Job& job)
{ {
Log(Debug::Debug) << "Process job for agent=(" << std::fixed << std::setprecision(2) << job.mAgentHalfExtents << ")"; Log(Debug::Debug) << "Process job for agent=(" << std::fixed << std::setprecision(2) << job.mAgentHalfExtents << ")"
" by thread=" << std::this_thread::get_id();
const auto start = std::chrono::steady_clock::now(); const auto start = std::chrono::steady_clock::now();
@ -170,11 +181,13 @@ namespace DetourNavigator
const auto locked = navMeshCacheItem->lockConst(); const auto locked = navMeshCacheItem->lockConst();
Log(Debug::Debug) << std::fixed << std::setprecision(2) << Log(Debug::Debug) << std::fixed << std::setprecision(2) <<
"Cache updated for agent=(" << job.mAgentHalfExtents << ")" << "Cache updated for agent=(" << job.mAgentHalfExtents << ")" <<
" tile=" << job.mChangedTile <<
" status=" << status << " status=" << status <<
" generation=" << locked->getGeneration() << " generation=" << locked->getGeneration() <<
" revision=" << locked->getNavMeshRevision() << " revision=" << locked->getNavMeshRevision() <<
" time=" << std::chrono::duration_cast<FloatMs>(finish - start).count() << "ms" << " time=" << std::chrono::duration_cast<FloatMs>(finish - start).count() << "ms" <<
" total_time=" << std::chrono::duration_cast<FloatMs>(finish - firstStart).count() << "ms"; " total_time=" << std::chrono::duration_cast<FloatMs>(finish - firstStart).count() << "ms"
" thread=" << std::this_thread::get_id();
return isSuccess(status); return isSuccess(status);
} }
@ -188,42 +201,57 @@ namespace DetourNavigator
while (true) while (true)
{ {
const auto hasJob = [&] { return !mJobs.empty() || !threadQueue.mPushed.empty(); }; const auto hasJob = [&] {
return (!mJobs.empty() && mJobs.top().mProcessTime <= std::chrono::steady_clock::now())
|| !threadQueue.mJobs.empty();
};
if (!mHasJob.wait_for(lock, std::chrono::milliseconds(10), hasJob)) if (!mHasJob.wait_for(lock, std::chrono::milliseconds(10), hasJob))
{ {
mFirstStart.lock()->reset(); mFirstStart.lock()->reset();
mDone.notify_all(); if (mJobs.empty() && getTotalThreadJobsUnsafe() == 0)
mDone.notify_all();
return boost::none; return boost::none;
} }
Log(Debug::Debug) << "Got " << mJobs.size() << " navigator jobs and " Log(Debug::Debug) << "Got " << mJobs.size() << " navigator jobs and "
<< threadQueue.mJobs.size() << " thread jobs"; << threadQueue.mJobs.size() << " thread jobs by thread=" << std::this_thread::get_id();
auto job = threadQueue.mJobs.empty() auto job = threadQueue.mJobs.empty()
? getJob(mJobs, mPushed) ? getJob(mJobs, mPushed, true)
: getJob(threadQueue.mJobs, threadQueue.mPushed); : getJob(threadQueue.mJobs, threadQueue.mPushed, false);
const auto owner = lockTile(job.mAgentHalfExtents, job.mChangedTile); if (!job)
continue;
const auto owner = lockTile(job->mAgentHalfExtents, job->mChangedTile);
if (owner == threadId) if (owner == threadId)
return job; return job;
postThreadJob(std::move(job), mThreadsQueues[owner]); postThreadJob(std::move(*job), mThreadsQueues[owner]);
} }
} }
AsyncNavMeshUpdater::Job AsyncNavMeshUpdater::getJob(Jobs& jobs, Pushed& pushed) boost::optional<AsyncNavMeshUpdater::Job> AsyncNavMeshUpdater::getJob(Jobs& jobs, Pushed& pushed, bool changeLastUpdate)
{ {
auto job = jobs.top(); const auto now = std::chrono::steady_clock::now();
if (jobs.top().mProcessTime > now)
return {};
Job job = std::move(jobs.top());
jobs.pop(); jobs.pop();
if (changeLastUpdate && job.mChangeType == ChangeType::update)
mLastUpdates[job.mAgentHalfExtents][job.mChangedTile] = now;
const auto it = pushed.find(job.mAgentHalfExtents); const auto it = pushed.find(job.mAgentHalfExtents);
it->second.erase(job.mChangedTile); it->second.erase(job.mChangedTile);
if (it->second.empty()) if (it->second.empty())
pushed.erase(it); pushed.erase(it);
return job; return {std::move(job)};
} }
void AsyncNavMeshUpdater::writeDebugFiles(const Job& job, const RecastMesh* recastMesh) const void AsyncNavMeshUpdater::writeDebugFiles(const Job& job, const RecastMesh* recastMesh) const
@ -326,5 +354,37 @@ namespace DetourNavigator
if (agent->second.empty()) if (agent->second.empty())
locked->erase(agent); locked->erase(agent);
if (locked->empty())
mProcessed.notify_all();
}
std::size_t AsyncNavMeshUpdater::getTotalThreadJobsUnsafe() const
{
return std::accumulate(mThreadsQueues.begin(), mThreadsQueues.end(), std::size_t(0),
[] (auto r, const auto& v) { return r + v.second.mJobs.size(); });
}
void AsyncNavMeshUpdater::cleanupLastUpdates()
{
const auto now = std::chrono::steady_clock::now();
const std::lock_guard<std::mutex> lock(mMutex);
for (auto agent = mLastUpdates.begin(); agent != mLastUpdates.end();)
{
for (auto tile = agent->second.begin(); tile != agent->second.end();)
{
if (now - tile->second > mSettings.get().mMinUpdateInterval)
tile = agent->second.erase(tile);
else
++tile;
}
if (agent->second.empty())
agent = mLastUpdates.erase(agent);
else
++agent;
}
} }
} }

View file

@ -32,6 +32,21 @@ namespace DetourNavigator
update = 3, update = 3,
}; };
inline std::ostream& operator <<(std::ostream& stream, ChangeType value)
{
switch (value) {
case ChangeType::remove:
return stream << "ChangeType::remove";
case ChangeType::mixed:
return stream << "ChangeType::mixed";
case ChangeType::add:
return stream << "ChangeType::add";
case ChangeType::update:
return stream << "ChangeType::update";
}
return stream << "ChangeType::" << static_cast<int>(value);
}
class AsyncNavMeshUpdater class AsyncNavMeshUpdater
{ {
public: public:
@ -56,10 +71,11 @@ namespace DetourNavigator
ChangeType mChangeType; ChangeType mChangeType;
int mDistanceToPlayer; int mDistanceToPlayer;
int mDistanceToOrigin; int mDistanceToOrigin;
std::chrono::steady_clock::time_point mProcessTime;
std::tuple<unsigned, ChangeType, int, int> getPriority() const std::tuple<std::chrono::steady_clock::time_point, unsigned, ChangeType, int, int> getPriority() const
{ {
return std::make_tuple(mTryNumber, mChangeType, mDistanceToPlayer, mDistanceToOrigin); return std::make_tuple(mProcessTime, mTryNumber, mChangeType, mDistanceToPlayer, mDistanceToOrigin);
} }
friend inline bool operator <(const Job& lhs, const Job& rhs) friend inline bool operator <(const Job& lhs, const Job& rhs)
@ -86,12 +102,14 @@ namespace DetourNavigator
mutable std::mutex mMutex; mutable std::mutex mMutex;
std::condition_variable mHasJob; std::condition_variable mHasJob;
std::condition_variable mDone; std::condition_variable mDone;
std::condition_variable mProcessed;
Jobs mJobs; Jobs mJobs;
std::map<osg::Vec3f, std::set<TilePosition>> mPushed; std::map<osg::Vec3f, std::set<TilePosition>> mPushed;
Misc::ScopeGuarded<TilePosition> mPlayerTile; Misc::ScopeGuarded<TilePosition> mPlayerTile;
Misc::ScopeGuarded<boost::optional<std::chrono::steady_clock::time_point>> mFirstStart; Misc::ScopeGuarded<boost::optional<std::chrono::steady_clock::time_point>> mFirstStart;
NavMeshTilesCache mNavMeshTilesCache; NavMeshTilesCache mNavMeshTilesCache;
Misc::ScopeGuarded<std::map<osg::Vec3f, std::map<TilePosition, std::thread::id>>> mProcessingTiles; Misc::ScopeGuarded<std::map<osg::Vec3f, std::map<TilePosition, std::thread::id>>> mProcessingTiles;
std::map<osg::Vec3f, std::map<TilePosition, std::chrono::steady_clock::time_point>> mLastUpdates;
std::map<std::thread::id, Queue> mThreadsQueues; std::map<std::thread::id, Queue> mThreadsQueues;
std::vector<std::thread> mThreads; std::vector<std::thread> mThreads;
@ -101,7 +119,7 @@ namespace DetourNavigator
boost::optional<Job> getNextJob(); boost::optional<Job> getNextJob();
static Job getJob(Jobs& jobs, Pushed& pushed); boost::optional<Job> getJob(Jobs& jobs, Pushed& pushed, bool changeLastUpdate);
void postThreadJob(Job&& job, Queue& queue); void postThreadJob(Job&& job, Queue& queue);
@ -114,6 +132,10 @@ namespace DetourNavigator
std::thread::id lockTile(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile); std::thread::id lockTile(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile);
void unlockTile(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile); void unlockTile(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile);
inline std::size_t getTotalThreadJobsUnsafe() const;
void cleanupLastUpdates();
}; };
} }

View file

@ -56,12 +56,8 @@ namespace DetourNavigator
bool NavMeshManager::updateObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, bool NavMeshManager::updateObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform,
const AreaType areaType) const AreaType areaType)
{ {
const auto changedTiles = mRecastMeshManager.updateObject(id, shape, transform, areaType); return mRecastMeshManager.updateObject(id, shape, transform, areaType,
if (changedTiles.empty()) [&] (const TilePosition& tile) { addChangedTile(tile, ChangeType::update); });
return false;
for (const auto& tile : changedTiles)
addChangedTile(tile, ChangeType::update);
return true;
} }
bool NavMeshManager::removeObject(const ObjectId id) bool NavMeshManager::removeObject(const ObjectId id)
@ -195,7 +191,7 @@ namespace DetourNavigator
mAsyncNavMeshUpdater.post(agentHalfExtents, cached, playerTile, tilesToPost); mAsyncNavMeshUpdater.post(agentHalfExtents, cached, playerTile, tilesToPost);
if (changedTiles != mChangedTiles.end()) if (changedTiles != mChangedTiles.end())
changedTiles->second.clear(); changedTiles->second.clear();
Log(Debug::Debug) << "cache update posted for agent=" << agentHalfExtents << Log(Debug::Debug) << "Cache update posted for agent=" << agentHalfExtents <<
" playerTile=" << lastPlayerTile->second << " playerTile=" << lastPlayerTile->second <<
" recastMeshManagerRevision=" << lastRevision; " recastMeshManagerRevision=" << lastRevision;
} }

View file

@ -13,6 +13,7 @@
#include <boost/optional.hpp> #include <boost/optional.hpp>
#include <algorithm>
#include <map> #include <map>
#include <mutex> #include <mutex>
#include <unordered_map> #include <unordered_map>

View file

@ -40,6 +40,7 @@ namespace DetourNavigator
navigatorSettings.mNavMeshPathPrefix = ::Settings::Manager::getString("nav 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.mEnableRecastMeshFileNameRevision = ::Settings::Manager::getBool("enable recast mesh file name revision", "Navigator");
navigatorSettings.mEnableNavMeshFileNameRevision = ::Settings::Manager::getBool("enable nav mesh file name revision", "Navigator"); navigatorSettings.mEnableNavMeshFileNameRevision = ::Settings::Manager::getBool("enable nav mesh file name revision", "Navigator");
navigatorSettings.mMinUpdateInterval = std::chrono::milliseconds(::Settings::Manager::getInt("min update interval ms", "Navigator"));
return navigatorSettings; return navigatorSettings;
} }

View file

@ -4,6 +4,7 @@
#include <boost/optional.hpp> #include <boost/optional.hpp>
#include <string> #include <string>
#include <chrono>
namespace DetourNavigator namespace DetourNavigator
{ {
@ -38,6 +39,7 @@ namespace DetourNavigator
std::size_t mTrianglesPerChunk = 0; std::size_t mTrianglesPerChunk = 0;
std::string mRecastMeshPathPrefix; std::string mRecastMeshPathPrefix;
std::string mNavMeshPathPrefix; std::string mNavMeshPathPrefix;
std::chrono::milliseconds mMinUpdateInterval;
}; };
boost::optional<Settings> makeSettingsFromSettingsManager(); boost::optional<Settings> makeSettingsFromSettingsManager();

View file

@ -31,43 +31,6 @@ namespace DetourNavigator
return result; return result;
} }
std::vector<TilePosition> TileCachedRecastMeshManager::updateObject(const ObjectId id, const btCollisionShape& shape,
const btTransform& transform, const AreaType areaType)
{
const auto object = mObjectsTilesPositions.find(id);
if (object == mObjectsTilesPositions.end())
return std::vector<TilePosition>();
auto& currentTiles = object->second;
const auto border = getBorderSize(mSettings);
std::vector<TilePosition> changedTiles;
std::set<TilePosition> newTiles;
{
auto tiles = mTiles.lock();
const auto onTilePosition = [&] (const TilePosition& tilePosition)
{
if (currentTiles.count(tilePosition))
{
newTiles.insert(tilePosition);
if (updateTile(id, transform, areaType, tilePosition, tiles.get()))
changedTiles.push_back(tilePosition);
}
else if (addTile(id, shape, transform, areaType, tilePosition, border, tiles.get()))
{
newTiles.insert(tilePosition);
changedTiles.push_back(tilePosition);
}
};
getTilesPositions(shape, transform, mSettings, onTilePosition);
for (const auto& tile : currentTiles)
if (!newTiles.count(tile) && removeTile(id, tile, tiles.get()))
changedTiles.push_back(tile);
}
std::swap(currentTiles, newTiles);
if (!changedTiles.empty())
++mRevision;
return changedTiles;
}
boost::optional<RemovedRecastMeshObject> TileCachedRecastMeshManager::removeObject(const ObjectId id) boost::optional<RemovedRecastMeshObject> TileCachedRecastMeshManager::removeObject(const ObjectId id)
{ {
const auto object = mObjectsTilesPositions.find(id); const auto object = mObjectsTilesPositions.find(id);

View file

@ -3,6 +3,8 @@
#include "cachedrecastmeshmanager.hpp" #include "cachedrecastmeshmanager.hpp"
#include "tileposition.hpp" #include "tileposition.hpp"
#include "settingsutils.hpp"
#include "gettilespositions.hpp"
#include <components/misc/guarded.hpp> #include <components/misc/guarded.hpp>
@ -20,8 +22,52 @@ namespace DetourNavigator
bool addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, bool addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform,
const AreaType areaType); const AreaType areaType);
std::vector<TilePosition> updateObject(const ObjectId id, const btCollisionShape& shape, template <class OnChangedTile>
const btTransform& transform, const AreaType areaType); bool updateObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform,
const AreaType areaType, OnChangedTile&& onChangedTile)
{
const auto object = mObjectsTilesPositions.find(id);
if (object == mObjectsTilesPositions.end())
return false;
auto& currentTiles = object->second;
const auto border = getBorderSize(mSettings);
bool changed = false;
std::set<TilePosition> newTiles;
{
auto tiles = mTiles.lock();
const auto onTilePosition = [&] (const TilePosition& tilePosition)
{
if (currentTiles.count(tilePosition))
{
newTiles.insert(tilePosition);
if (updateTile(id, transform, areaType, tilePosition, tiles.get()))
{
onChangedTile(tilePosition);
changed = true;
}
}
else if (addTile(id, shape, transform, areaType, tilePosition, border, tiles.get()))
{
newTiles.insert(tilePosition);
onChangedTile(tilePosition);
changed = true;
}
};
getTilesPositions(shape, transform, mSettings, onTilePosition);
for (const auto& tile : currentTiles)
{
if (!newTiles.count(tile) && removeTile(id, tile, tiles.get()))
{
onChangedTile(tile);
changed = true;
}
}
}
std::swap(currentTiles, newTiles);
if (changed)
++mRevision;
return changed;
}
boost::optional<RemovedRecastMeshObject> removeObject(const ObjectId id); boost::optional<RemovedRecastMeshObject> removeObject(const ObjectId id);

View file

@ -32,6 +32,14 @@ ConfigurationManager::ConfigurationManager(bool silent)
boost::filesystem::create_directories(mFixedPath.getUserDataPath()); boost::filesystem::create_directories(mFixedPath.getUserDataPath());
mLogPath = mFixedPath.getUserConfigPath(); mLogPath = mFixedPath.getUserConfigPath();
mScreenshotPath = mFixedPath.getUserDataPath() / "screenshots";
// probably not necessary but validate the creation of the screenshots directory and fallback to the original behavior if it fails
boost::system::error_code dirErr;
if (!boost::filesystem::create_directories(mScreenshotPath, dirErr) && !boost::filesystem::is_directory(mScreenshotPath)) {
mScreenshotPath = mFixedPath.getUserDataPath();
}
} }
ConfigurationManager::~ConfigurationManager() ConfigurationManager::~ConfigurationManager()
@ -196,4 +204,9 @@ const boost::filesystem::path& ConfigurationManager::getLogPath() const
return mLogPath; return mLogPath;
} }
const boost::filesystem::path& ConfigurationManager::getScreenshotPath() const
{
return mScreenshotPath;
}
} /* namespace Cfg */ } /* namespace Cfg */

View file

@ -41,6 +41,7 @@ struct ConfigurationManager
const boost::filesystem::path& getCachePath() const; const boost::filesystem::path& getCachePath() const;
const boost::filesystem::path& getLogPath() const; const boost::filesystem::path& getLogPath() const;
const boost::filesystem::path& getScreenshotPath() const;
private: private:
typedef Files::FixedPath<> FixedPathType; typedef Files::FixedPath<> FixedPathType;
@ -57,6 +58,7 @@ struct ConfigurationManager
FixedPathType mFixedPath; FixedPathType mFixedPath;
boost::filesystem::path mLogPath; boost::filesystem::path mLogPath;
boost::filesystem::path mScreenshotPath;
TokensMappingContainer mTokensMapping; TokensMappingContainer mTokensMapping;

View file

@ -28,22 +28,6 @@ namespace Interpreter
return; return;
} }
case 1:
{
int opcode = (code>>24) & 0x3f;
unsigned int arg0 = (code>>16) & 0xfff;
unsigned int arg1 = code & 0xfff;
std::map<int, Opcode2 *>::iterator iter = mSegment1.find (opcode);
if (iter==mSegment1.end())
abortUnknownCode (1, opcode);
iter->second->execute (mRuntime, arg0, arg1);
return;
}
case 2: case 2:
{ {
int opcode = (code>>20) & 0x3ff; int opcode = (code>>20) & 0x3ff;
@ -79,22 +63,6 @@ namespace Interpreter
return; return;
} }
case 0x31:
{
int opcode = (code>>16) & 0x3ff;
unsigned int arg0 = (code>>8) & 0xff;
unsigned int arg1 = code & 0xff;
std::map<int, Opcode2 *>::iterator iter = mSegment4.find (opcode);
if (iter==mSegment4.end())
abortUnknownCode (4, opcode);
iter->second->execute (mRuntime, arg0, arg1);
return;
}
case 0x32: case 0x32:
{ {
int opcode = code & 0x3ffffff; int opcode = code & 0x3ffffff;
@ -161,10 +129,6 @@ namespace Interpreter
iter!=mSegment0.end(); ++iter) iter!=mSegment0.end(); ++iter)
delete iter->second; delete iter->second;
for (std::map<int, Opcode2 *>::iterator iter (mSegment1.begin());
iter!=mSegment1.end(); ++iter)
delete iter->second;
for (std::map<int, Opcode1 *>::iterator iter (mSegment2.begin()); for (std::map<int, Opcode1 *>::iterator iter (mSegment2.begin());
iter!=mSegment2.end(); ++iter) iter!=mSegment2.end(); ++iter)
delete iter->second; delete iter->second;
@ -173,10 +137,6 @@ namespace Interpreter
iter!=mSegment3.end(); ++iter) iter!=mSegment3.end(); ++iter)
delete iter->second; delete iter->second;
for (std::map<int, Opcode2 *>::iterator iter (mSegment4.begin());
iter!=mSegment4.end(); ++iter)
delete iter->second;
for (std::map<int, Opcode0 *>::iterator iter (mSegment5.begin()); for (std::map<int, Opcode0 *>::iterator iter (mSegment5.begin());
iter!=mSegment5.end(); ++iter) iter!=mSegment5.end(); ++iter)
delete iter->second; delete iter->second;
@ -188,12 +148,6 @@ namespace Interpreter
mSegment0.insert (std::make_pair (code, opcode)); mSegment0.insert (std::make_pair (code, opcode));
} }
void Interpreter::installSegment1 (int code, Opcode2 *opcode)
{
assert(mSegment1.find(code) == mSegment1.end());
mSegment1.insert (std::make_pair (code, opcode));
}
void Interpreter::installSegment2 (int code, Opcode1 *opcode) void Interpreter::installSegment2 (int code, Opcode1 *opcode)
{ {
assert(mSegment2.find(code) == mSegment2.end()); assert(mSegment2.find(code) == mSegment2.end());
@ -206,12 +160,6 @@ namespace Interpreter
mSegment3.insert (std::make_pair (code, opcode)); mSegment3.insert (std::make_pair (code, opcode));
} }
void Interpreter::installSegment4 (int code, Opcode2 *opcode)
{
assert(mSegment4.find(code) == mSegment4.end());
mSegment4.insert (std::make_pair (code, opcode));
}
void Interpreter::installSegment5 (int code, Opcode0 *opcode) void Interpreter::installSegment5 (int code, Opcode0 *opcode)
{ {
assert(mSegment5.find(code) == mSegment5.end()); assert(mSegment5.find(code) == mSegment5.end());

View file

@ -11,7 +11,6 @@ namespace Interpreter
{ {
class Opcode0; class Opcode0;
class Opcode1; class Opcode1;
class Opcode2;
class Interpreter class Interpreter
{ {
@ -19,10 +18,8 @@ namespace Interpreter
bool mRunning; bool mRunning;
Runtime mRuntime; Runtime mRuntime;
std::map<int, Opcode1 *> mSegment0; std::map<int, Opcode1 *> mSegment0;
std::map<int, Opcode2 *> mSegment1;
std::map<int, Opcode1 *> mSegment2; std::map<int, Opcode1 *> mSegment2;
std::map<int, Opcode1 *> mSegment3; std::map<int, Opcode1 *> mSegment3;
std::map<int, Opcode2 *> mSegment4;
std::map<int, Opcode0 *> mSegment5; std::map<int, Opcode0 *> mSegment5;
// not implemented // not implemented
@ -48,18 +45,12 @@ namespace Interpreter
void installSegment0 (int code, Opcode1 *opcode); void installSegment0 (int code, Opcode1 *opcode);
///< ownership of \a opcode is transferred to *this. ///< ownership of \a opcode is transferred to *this.
void installSegment1 (int code, Opcode2 *opcode);
///< ownership of \a opcode is transferred to *this.
void installSegment2 (int code, Opcode1 *opcode); void installSegment2 (int code, Opcode1 *opcode);
///< ownership of \a opcode is transferred to *this. ///< ownership of \a opcode is transferred to *this.
void installSegment3 (int code, Opcode1 *opcode); void installSegment3 (int code, Opcode1 *opcode);
///< ownership of \a opcode is transferred to *this. ///< ownership of \a opcode is transferred to *this.
void installSegment4 (int code, Opcode2 *opcode);
///< ownership of \a opcode is transferred to *this.
void installSegment5 (int code, Opcode0 *opcode); void installSegment5 (int code, Opcode0 *opcode);
///< ownership of \a opcode is transferred to *this. ///< ownership of \a opcode is transferred to *this.

View file

@ -25,16 +25,6 @@ namespace Interpreter
virtual ~Opcode1() {} virtual ~Opcode1() {}
}; };
/// opcode for 2 arguments
class Opcode2
{
public:
virtual void execute (Runtime& runtime, unsigned int arg1, unsigned int arg2) = 0;
virtual ~Opcode2() {}
};
} }
#endif #endif

View file

@ -3,6 +3,7 @@
#include <mutex> #include <mutex>
#include <memory> #include <memory>
#include <condition_variable>
namespace Misc namespace Misc
{ {
@ -79,6 +80,13 @@ namespace Misc
return Locked<const T>(mMutex, mValue); return Locked<const T>(mMutex, mValue);
} }
template <class Predicate>
void wait(std::condition_variable& cv, Predicate&& predicate)
{
std::unique_lock<std::mutex> lock(mMutex);
cv.wait(lock, [&] { return predicate(mValue); });
}
private: private:
std::mutex mMutex; std::mutex mMutex;
T mValue; T mValue;

View file

@ -170,6 +170,17 @@ namespace NifOsg
class CollisionSwitch : public osg::MatrixTransform class CollisionSwitch : public osg::MatrixTransform
{ {
public: public:
CollisionSwitch() : osg::MatrixTransform()
{
}
CollisionSwitch(const CollisionSwitch& copy, const osg::CopyOp& copyop)
: osg::MatrixTransform(copy, copyop)
{
}
META_Node(NifOsg, CollisionSwitch)
CollisionSwitch(const osg::Matrixf& transformations, bool enabled) : osg::MatrixTransform(transformations) CollisionSwitch(const osg::Matrixf& transformations, bool enabled) : osg::MatrixTransform(transformations)
{ {
setEnabled(enabled); setEnabled(enabled);
@ -177,7 +188,7 @@ namespace NifOsg
void setEnabled(bool enabled) void setEnabled(bool enabled)
{ {
setNodeMask(enabled ? ~0 : 0); setNodeMask(enabled ? ~0 : Loader::getIntersectionDisabledNodeMask());
} }
}; };
@ -204,6 +215,18 @@ namespace NifOsg
return sHiddenNodeMask; return sHiddenNodeMask;
} }
unsigned int Loader::sIntersectionDisabledNodeMask = ~0;
void Loader::setIntersectionDisabledNodeMask(unsigned int mask)
{
sIntersectionDisabledNodeMask = mask;
}
unsigned int Loader::getIntersectionDisabledNodeMask()
{
return sIntersectionDisabledNodeMask;
}
class LoaderImpl class LoaderImpl
{ {
public: public:
@ -219,6 +242,9 @@ namespace NifOsg
size_t mFirstRootTextureIndex = -1; size_t mFirstRootTextureIndex = -1;
bool mFoundFirstRootTexturingProperty = false; bool mFoundFirstRootTexturingProperty = false;
// This is used to queue emitters that weren't attached to their node yet.
std::vector<std::pair<size_t, osg::ref_ptr<Emitter>>> mEmitterQueue;
static void loadKf(Nif::NIFFilePtr nif, KeyframeHolder& target) static void loadKf(Nif::NIFFilePtr nif, KeyframeHolder& target)
{ {
if(nif->numRoots() < 1) if(nif->numRoots() < 1)
@ -290,6 +316,9 @@ namespace NifOsg
osg::ref_ptr<osg::Node> created = handleNode(nifNode, nullptr, imageManager, std::vector<unsigned int>(), 0, false, false, false, &textkeys->mTextKeys); osg::ref_ptr<osg::Node> created = handleNode(nifNode, nullptr, imageManager, std::vector<unsigned int>(), 0, false, false, false, &textkeys->mTextKeys);
// Attach particle emitters to their nodes which should all be loaded by now.
handleQueuedParticleEmitters(created, nif);
if (nif->getUseSkinning()) if (nif->getUseSkinning())
{ {
osg::ref_ptr<SceneUtil::Skeleton> skel = new SceneUtil::Skeleton; osg::ref_ptr<SceneUtil::Skeleton> skel = new SceneUtil::Skeleton;
@ -460,17 +489,8 @@ namespace NifOsg
osg::ref_ptr<osg::Group> node; osg::ref_ptr<osg::Group> node;
osg::Object::DataVariance dataVariance = osg::Object::UNSPECIFIED; osg::Object::DataVariance dataVariance = osg::Object::UNSPECIFIED;
// TODO: it is unclear how to handle transformations of LOD nodes and controllers for them.
switch (nifNode->recType) switch (nifNode->recType)
{ {
case Nif::RC_NiLODNode:
{
const Nif::NiLODNode* niLodNode = static_cast<const Nif::NiLODNode*>(nifNode);
node = handleLodNode(niLodNode);
dataVariance = osg::Object::DYNAMIC;
break;
}
case Nif::RC_NiSwitchNode:
case Nif::RC_NiAutoNormalParticles: case Nif::RC_NiAutoNormalParticles:
case Nif::RC_NiRotatingParticles: case Nif::RC_NiRotatingParticles:
// Leaf nodes in the NIF hierarchy, so won't be able to dynamically attach children. // Leaf nodes in the NIF hierarchy, so won't be able to dynamically attach children.
@ -591,7 +611,10 @@ namespace NifOsg
{ {
bool hasVisController = false; bool hasVisController = false;
for (Nif::ControllerPtr ctrl = nifNode->controller; !ctrl.empty(); ctrl = ctrl->next) for (Nif::ControllerPtr ctrl = nifNode->controller; !ctrl.empty(); ctrl = ctrl->next)
hasVisController = (ctrl->recType == Nif::RC_NiVisController); {
if (hasVisController |= (ctrl->recType == Nif::RC_NiVisController))
break;
}
if (!hasVisController) if (!hasVisController)
skipMeshes = true; // skip child meshes, but still create the child node hierarchy for animating collision shapes skipMeshes = true; // skip child meshes, but still create the child node hierarchy for animating collision shapes
@ -648,6 +671,11 @@ namespace NifOsg
&& !nifNode->controller.empty() && node->getDataVariance() == osg::Object::DYNAMIC) && !nifNode->controller.empty() && node->getDataVariance() == osg::Object::DYNAMIC)
handleNodeControllers(nifNode, static_cast<osg::MatrixTransform*>(node.get()), animflags); handleNodeControllers(nifNode, static_cast<osg::MatrixTransform*>(node.get()), animflags);
// LOD and Switch nodes must be wrapped by a transform (the current node) to support transformations properly
// and we need to attach their children to the osg::LOD/osg::Switch nodes
// but we must return that transform to the caller of handleNode instead of the actual LOD/Switch nodes.
osg::ref_ptr<osg::Group> currentNode = node;
if (nifNode->recType == Nif::RC_NiSwitchNode) if (nifNode->recType == Nif::RC_NiSwitchNode)
{ {
const Nif::NiSwitchNode* niSwitchNode = static_cast<const Nif::NiSwitchNode*>(nifNode); const Nif::NiSwitchNode* niSwitchNode = static_cast<const Nif::NiSwitchNode*>(nifNode);
@ -658,7 +686,14 @@ namespace NifOsg
else if (niSwitchNode->name == Constants::HerbalismLabel && !SceneUtil::hasUserDescription(rootNode, Constants::HerbalismLabel)) else if (niSwitchNode->name == Constants::HerbalismLabel && !SceneUtil::hasUserDescription(rootNode, Constants::HerbalismLabel))
rootNode->getOrCreateUserDataContainer()->addDescription(Constants::HerbalismLabel); rootNode->getOrCreateUserDataContainer()->addDescription(Constants::HerbalismLabel);
node = switchNode; currentNode = switchNode;
}
else if (nifNode->recType == Nif::RC_NiLODNode)
{
const Nif::NiLODNode* niLodNode = static_cast<const Nif::NiLODNode*>(nifNode);
osg::ref_ptr<osg::LOD> lodNode = handleLodNode(niLodNode);
node->addChild(lodNode);
currentNode = lodNode;
} }
const Nif::NiNode *ninode = dynamic_cast<const Nif::NiNode*>(nifNode); const Nif::NiNode *ninode = dynamic_cast<const Nif::NiNode*>(nifNode);
@ -668,14 +703,14 @@ namespace NifOsg
for (size_t i = 0; i < effects.length(); ++i) for (size_t i = 0; i < effects.length(); ++i)
{ {
if (!effects[i].empty()) if (!effects[i].empty())
handleEffect(effects[i].getPtr(), node, imageManager); handleEffect(effects[i].getPtr(), currentNode, imageManager);
} }
const Nif::NodeList &children = ninode->children; const Nif::NodeList &children = ninode->children;
for(size_t i = 0;i < children.length();++i) for(size_t i = 0;i < children.length();++i)
{ {
if(!children[i].empty()) if(!children[i].empty())
handleNode(children[i].getPtr(), node, imageManager, boundTextures, animflags, skipMeshes, hasMarkers, isAnimated, textKeys, rootNode); handleNode(children[i].getPtr(), currentNode, imageManager, boundTextures, animflags, skipMeshes, hasMarkers, isAnimated, textKeys, rootNode);
} }
} }
@ -979,6 +1014,27 @@ namespace NifOsg
return emitter; return emitter;
} }
void handleQueuedParticleEmitters(osg::Node* rootNode, Nif::NIFFilePtr nif)
{
for (const auto& emitterPair : mEmitterQueue)
{
size_t recIndex = emitterPair.first;
FindGroupByRecIndex findEmitterNode(recIndex);
rootNode->accept(findEmitterNode);
osg::Group* emitterNode = findEmitterNode.mFound;
if (!emitterNode)
{
nif->warn("Failed to find particle emitter emitter node (node record index " + std::to_string(recIndex) + ")");
continue;
}
// Emitter attached to the emitter node. Note one side effect of the emitter using the CullVisitor is that hiding its node
// actually causes the emitter to stop firing. Convenient, because MW behaves this way too!
emitterNode->addChild(emitterPair.second);
}
mEmitterQueue.clear();
}
void handleParticleSystem(const Nif::Node *nifNode, osg::Group *parentNode, SceneUtil::CompositeStateSetUpdater* composite, int animflags, osg::Node* rootNode) void handleParticleSystem(const Nif::Node *nifNode, osg::Group *parentNode, SceneUtil::CompositeStateSetUpdater* composite, int animflags, osg::Node* rootNode)
{ {
osg::ref_ptr<ParticleSystem> partsys (new ParticleSystem); osg::ref_ptr<ParticleSystem> partsys (new ParticleSystem);
@ -1028,22 +1084,10 @@ namespace NifOsg
emitter->setParticleSystem(partsys); emitter->setParticleSystem(partsys);
emitter->setReferenceFrame(osgParticle::ParticleProcessor::RELATIVE_RF); emitter->setReferenceFrame(osgParticle::ParticleProcessor::RELATIVE_RF);
// Note: we assume that the Emitter node is placed *before* the Particle node in the scene graph. // The emitter node may not actually be handled yet, so let's delay attaching the emitter to a later moment.
// This seems to be true for all NIF files in the game that I've checked, suggesting that NIFs work similar to OSG with regards to update order. // If the emitter node is placed later than the particle node, it'll have a single frame delay in particle processing.
// If something ever violates this assumption, the worst that could happen is the culling being one frame late, which wouldn't be a disaster. // But that shouldn't be a game-breaking issue.
mEmitterQueue.emplace_back(partctrl->emitter->recIndex, emitter);
FindGroupByRecIndex find (partctrl->emitter->recIndex);
rootNode->accept(find);
if (!find.mFound)
{
Log(Debug::Info) << "can't find emitter node, wrong node order? in " << mFilename;
return;
}
osg::Group* emitterNode = find.mFound;
// Emitter attached to the emitter node. Note one side effect of the emitter using the CullVisitor is that hiding its node
// actually causes the emitter to stop firing. Convenient, because MW behaves this way too!
emitterNode->addChild(emitter);
osg::ref_ptr<ParticleSystemController> callback(new ParticleSystemController(partctrl)); osg::ref_ptr<ParticleSystemController> callback(new ParticleSystemController(partctrl));
setupController(partctrl, callback, animflags); setupController(partctrl, callback, animflags);
@ -1060,14 +1104,14 @@ namespace NifOsg
partsys->update(0.0, nv); partsys->update(0.0, nv);
} }
// affectors must be attached *after* the emitter in the scene graph for correct update order // affectors should be attached *after* the emitter in the scene graph for correct update order
// attach to same node as the ParticleSystem, we need osgParticle Operators to get the correct // attach to same node as the ParticleSystem, we need osgParticle Operators to get the correct
// localToWorldMatrix for transforming to particle space // localToWorldMatrix for transforming to particle space
handleParticlePrograms(partctrl->affectors, partctrl->colliders, parentNode, partsys.get(), rf); handleParticlePrograms(partctrl->affectors, partctrl->colliders, parentNode, partsys.get(), rf);
std::vector<const Nif::Property*> drawableProps; std::vector<const Nif::Property*> drawableProps;
collectDrawableProperties(nifNode, drawableProps); collectDrawableProperties(nifNode, drawableProps);
applyDrawableProperties(parentNode, drawableProps, composite, true, animflags, true); applyDrawableProperties(parentNode, drawableProps, composite, true, animflags);
// particle system updater (after the emitters and affectors in the scene graph) // particle system updater (after the emitters and affectors in the scene graph)
// I think for correct culling needs to be *before* the ParticleSystem, though osg examples do it the other way // I think for correct culling needs to be *before* the ParticleSystem, though osg examples do it the other way
@ -1159,7 +1203,7 @@ namespace NifOsg
// above the actual renderable would be tedious. // above the actual renderable would be tedious.
std::vector<const Nif::Property*> drawableProps; std::vector<const Nif::Property*> drawableProps;
collectDrawableProperties(nifNode, drawableProps); collectDrawableProperties(nifNode, drawableProps);
applyDrawableProperties(parentNode, drawableProps, composite, vertexColorsPresent, animflags, false); applyDrawableProperties(parentNode, drawableProps, composite, vertexColorsPresent, animflags);
} }
void handleTriShape(const Nif::Node* nifNode, osg::Group* parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector<unsigned int>& boundTextures, int animflags) void handleTriShape(const Nif::Node* nifNode, osg::Group* parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector<unsigned int>& boundTextures, int animflags)
@ -1711,7 +1755,7 @@ namespace NifOsg
} }
void applyDrawableProperties(osg::Node* node, const std::vector<const Nif::Property*>& properties, SceneUtil::CompositeStateSetUpdater* composite, void applyDrawableProperties(osg::Node* node, const std::vector<const Nif::Property*>& properties, SceneUtil::CompositeStateSetUpdater* composite,
bool hasVertexColors, int animflags, bool particleMaterial) bool hasVertexColors, int animflags)
{ {
osg::StateSet* stateset = node->getOrCreateStateSet(); osg::StateSet* stateset = node->getOrCreateStateSet();

View file

@ -79,8 +79,14 @@ namespace NifOsg
static void setHiddenNodeMask(unsigned int mask); static void setHiddenNodeMask(unsigned int mask);
static unsigned int getHiddenNodeMask(); static unsigned int getHiddenNodeMask();
// Set the mask to use for nodes that ignore the crosshair intersection. The default is the default node mask.
// This is used for NiCollisionSwitch nodes with NiCollisionSwitch state set to disabled.
static void setIntersectionDisabledNodeMask(unsigned int mask);
static unsigned int getIntersectionDisabledNodeMask();
private: private:
static unsigned int sHiddenNodeMask; static unsigned int sHiddenNodeMask;
static unsigned int sIntersectionDisabledNodeMask;
static bool sShowMarkers; static bool sShowMarkers;
}; };

View file

@ -47,7 +47,7 @@ namespace Resource
ImageManager::ImageManager(const VFS::Manager *vfs) ImageManager::ImageManager(const VFS::Manager *vfs)
: ResourceManager(vfs) : ResourceManager(vfs)
, mWarningImage(createWarningImage()) , mWarningImage(createWarningImage())
, mOptions(new osgDB::Options("dds_flip dds_dxt1_detect_rgba")) , mOptions(new osgDB::Options("dds_flip dds_dxt1_detect_rgba ignoreTga2Fields"))
{ {
} }

View file

@ -8,6 +8,8 @@
#include <osgText/Text> #include <osgText/Text>
#include <osgDB/Registry>
#include <osgViewer/Viewer> #include <osgViewer/Viewer>
#include <osgViewer/Renderer> #include <osgViewer/Renderer>
@ -32,12 +34,16 @@ StatsHandler::StatsHandler():
_resourceStatsChildNum = 0; _resourceStatsChildNum = 0;
_font = osgMyGUI::DataManager::getInstance().getDataPath("DejaVuLGCSansMono.ttf"); if (osgDB::Registry::instance()->getReaderWriterForExtension("ttf"))
_font = osgMyGUI::DataManager::getInstance().getDataPath("DejaVuLGCSansMono.ttf");
} }
Profiler::Profiler() Profiler::Profiler()
{ {
_font = osgMyGUI::DataManager::getInstance().getDataPath("DejaVuLGCSansMono.ttf"); if (osgDB::Registry::instance()->getReaderWriterForExtension("ttf"))
_font = osgMyGUI::DataManager::getInstance().getDataPath("DejaVuLGCSansMono.ttf");
else
_font = "";
setKeyEventTogglesOnScreenStats(osgGA::GUIEventAdapter::KEY_F3); setKeyEventTogglesOnScreenStats(osgGA::GUIEventAdapter::KEY_F3);
} }

View file

@ -47,7 +47,7 @@ namespace SceneUtil
if (const osgParticle::ParticleSystemUpdater* updater = dynamic_cast<const osgParticle::ParticleSystemUpdater*>(node)) if (const osgParticle::ParticleSystemUpdater* updater = dynamic_cast<const osgParticle::ParticleSystemUpdater*>(node))
{ {
osgParticle::ParticleSystemUpdater* cloned = new osgParticle::ParticleSystemUpdater(*updater, osg::CopyOp::SHALLOW_COPY); osgParticle::ParticleSystemUpdater* cloned = new osgParticle::ParticleSystemUpdater(*updater, osg::CopyOp::SHALLOW_COPY);
mMap2[cloned] = updater->getParticleSystem(0); mUpdaterToOldPs[cloned] = updater->getParticleSystem(0);
return cloned; return cloned;
} }
return osg::CopyOp::operator()(node); return osg::CopyOp::operator()(node);
@ -69,7 +69,16 @@ namespace SceneUtil
osgParticle::ParticleProcessor* CopyOp::operator() (const osgParticle::ParticleProcessor* processor) const osgParticle::ParticleProcessor* CopyOp::operator() (const osgParticle::ParticleProcessor* processor) const
{ {
osgParticle::ParticleProcessor* cloned = osg::clone(processor, osg::CopyOp::DEEP_COPY_CALLBACKS); osgParticle::ParticleProcessor* cloned = osg::clone(processor, osg::CopyOp::DEEP_COPY_CALLBACKS);
mMap[cloned] = processor->getParticleSystem(); for (const auto& oldPsNewPsPair : mOldPsToNewPs)
{
if (processor->getParticleSystem() == oldPsNewPsPair.first)
{
cloned->setParticleSystem(oldPsNewPsPair.second);
return cloned;
}
}
mProcessorToOldPs[cloned] = processor->getParticleSystem();
return cloned; return cloned;
} }
@ -77,22 +86,25 @@ namespace SceneUtil
{ {
osgParticle::ParticleSystem* cloned = osg::clone(partsys, *this); osgParticle::ParticleSystem* cloned = osg::clone(partsys, *this);
for (std::map<osgParticle::ParticleProcessor*, const osgParticle::ParticleSystem*>::const_iterator it = mMap.begin(); it != mMap.end(); ++it) for (const auto& processorPsPair : mProcessorToOldPs)
{ {
if (it->second == partsys) if (processorPsPair.second == partsys)
{ {
it->first->setParticleSystem(cloned); processorPsPair.first->setParticleSystem(cloned);
} }
} }
for (std::map<osgParticle::ParticleSystemUpdater*, const osgParticle::ParticleSystem*>::const_iterator it = mMap2.begin(); it != mMap2.end(); ++it) for (const auto& updaterPsPair : mUpdaterToOldPs)
{ {
if (it->second == partsys) if (updaterPsPair.second == partsys)
{ {
osgParticle::ParticleSystemUpdater* updater = it->first; osgParticle::ParticleSystemUpdater* updater = updaterPsPair.first;
updater->removeParticleSystem(updater->getParticleSystem(0)); updater->removeParticleSystem(updater->getParticleSystem(0));
updater->addParticleSystem(cloned); updater->addParticleSystem(cloned);
} }
} }
// In rare situations a particle processor may be placed after the particle system in the scene graph.
mOldPsToNewPs[partsys] = cloned;
return cloned; return cloned;
} }

View file

@ -35,10 +35,11 @@ namespace SceneUtil
virtual osg::Object* operator ()(const osg::Object* node) const; virtual osg::Object* operator ()(const osg::Object* node) const;
private: private:
// maps new ParticleProcessor to their old ParticleSystem pointer // maps new pointers to their old pointers
// a little messy, but I think this should be the most efficient way // a little messy, but I think this should be the most efficient way
mutable std::map<osgParticle::ParticleProcessor*, const osgParticle::ParticleSystem*> mMap; mutable std::map<osgParticle::ParticleProcessor*, const osgParticle::ParticleSystem*> mProcessorToOldPs;
mutable std::map<osgParticle::ParticleSystemUpdater*, const osgParticle::ParticleSystem*> mMap2; mutable std::map<osgParticle::ParticleSystemUpdater*, const osgParticle::ParticleSystem*> mUpdaterToOldPs;
mutable std::map<const osgParticle::ParticleSystem*, osgParticle::ParticleSystem*> mOldPsToNewPs;
}; };
} }

View file

@ -7,9 +7,6 @@
#include <osg/Group> #include <osg/Group>
#include <osg/LineWidth> #include <osg/LineWidth>
#define OPENMW_TO_STRING(X) #X
#define OPENMW_LINE_STRING OPENMW_TO_STRING(__LINE__)
namespace namespace
{ {
using DetourNavigator::operator<<; using DetourNavigator::operator<<;
@ -121,6 +118,3 @@ namespace SceneUtil
mColors->push_back(value); mColors->push_back(value);
} }
} }
#undef OPENMW_TO_STRING
#undef OPENMW_LINE_STRING

View file

@ -872,6 +872,15 @@ void SceneUtil::MWShadowTechnique::setupCastingShader(Shader::ShaderManager & sh
_castingProgram->addShader(shaderManager.getShader("shadowcasting_vertex.glsl", Shader::ShaderManager::DefineMap(), osg::Shader::VERTEX)); _castingProgram->addShader(shaderManager.getShader("shadowcasting_vertex.glsl", Shader::ShaderManager::DefineMap(), osg::Shader::VERTEX));
_castingProgram->addShader(shaderManager.getShader("shadowcasting_fragment.glsl", Shader::ShaderManager::DefineMap(), osg::Shader::FRAGMENT)); _castingProgram->addShader(shaderManager.getShader("shadowcasting_fragment.glsl", Shader::ShaderManager::DefineMap(), osg::Shader::FRAGMENT));
_shadowMapAlphaTestDisableUniform = shaderManager.getShadowMapAlphaTestDisableUniform();
_shadowMapAlphaTestDisableUniform->setName("alphaTestShadows");
_shadowMapAlphaTestDisableUniform->setType(osg::Uniform::BOOL);
_shadowMapAlphaTestDisableUniform->set(false);
shaderManager.getShadowMapAlphaTestEnableUniform()->setName("alphaTestShadows");
shaderManager.getShadowMapAlphaTestEnableUniform()->setType(osg::Uniform::BOOL);
shaderManager.getShadowMapAlphaTestEnableUniform()->set(true);
} }
MWShadowTechnique::ViewDependentData* MWShadowTechnique::createViewDependentData(osgUtil::CullVisitor* /*cv*/) MWShadowTechnique::ViewDependentData* MWShadowTechnique::createViewDependentData(osgUtil::CullVisitor* /*cv*/)
@ -1570,6 +1579,7 @@ void MWShadowTechnique::createShaders()
// The casting program uses a sampler, so to avoid undefined behaviour, we must bind a dummy texture in case no other is supplied // The casting program uses a sampler, so to avoid undefined behaviour, we must bind a dummy texture in case no other is supplied
_shadowCastingStateSet->setTextureAttributeAndModes(0, _fallbackBaseTexture.get(), osg::StateAttribute::ON); _shadowCastingStateSet->setTextureAttributeAndModes(0, _fallbackBaseTexture.get(), osg::StateAttribute::ON);
_shadowCastingStateSet->addUniform(new osg::Uniform("useDiffuseMapForShadowAlpha", false)); _shadowCastingStateSet->addUniform(new osg::Uniform("useDiffuseMapForShadowAlpha", false));
_shadowCastingStateSet->addUniform(_shadowMapAlphaTestDisableUniform);
_shadowCastingStateSet->setMode(GL_DEPTH_CLAMP, osg::StateAttribute::ON); _shadowCastingStateSet->setMode(GL_DEPTH_CLAMP, osg::StateAttribute::ON);

View file

@ -286,6 +286,7 @@ namespace SceneUtil {
osg::ref_ptr<DebugHUD> _debugHud; osg::ref_ptr<DebugHUD> _debugHud;
osg::ref_ptr<osg::Program> _castingProgram; osg::ref_ptr<osg::Program> _castingProgram;
osg::ref_ptr<osg::Uniform> _shadowMapAlphaTestDisableUniform;
}; };
} }

View file

@ -735,20 +735,6 @@ bool Optimizer::CombineStaticTransformsVisitor::removeTransforms(osg::Node* node
// RemoveEmptyNodes. // RemoveEmptyNodes.
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
void Optimizer::RemoveEmptyNodesVisitor::apply(osg::Switch& switchNode)
{
// We should keep all switch child nodes since they reflect different switch states.
for (unsigned int i=0; i<switchNode.getNumChildren(); ++i)
traverse(*switchNode.getChild(i));
}
void Optimizer::RemoveEmptyNodesVisitor::apply(osg::LOD& lod)
{
// don't remove any direct children of the LOD because they are used to define each LOD level.
for (unsigned int i=0; i<lod.getNumChildren(); ++i)
traverse(*lod.getChild(i));
}
void Optimizer::RemoveEmptyNodesVisitor::apply(osg::Group& group) void Optimizer::RemoveEmptyNodesVisitor::apply(osg::Group& group)
{ {
if (group.getNumParents()>0) if (group.getNumParents()>0)
@ -787,8 +773,11 @@ void Optimizer::RemoveEmptyNodesVisitor::removeEmptyNodes()
++pitr) ++pitr)
{ {
osg::Group* parent = *pitr; osg::Group* parent = *pitr;
parent->removeChild(nodeToRemove.get()); if (!parent->asSwitch() && !dynamic_cast<osg::LOD*>(parent))
if (parent->getNumChildren()==0 && isOperationPermissibleForObject(parent)) newEmptyGroups.insert(parent); {
parent->removeChild(nodeToRemove.get());
if (parent->getNumChildren()==0 && isOperationPermissibleForObject(parent)) newEmptyGroups.insert(parent);
}
} }
} }

View file

@ -321,8 +321,6 @@ class Optimizer
BaseOptimizerVisitor(optimizer, REMOVE_REDUNDANT_NODES) {} BaseOptimizerVisitor(optimizer, REMOVE_REDUNDANT_NODES) {}
virtual void apply(osg::Group& group); virtual void apply(osg::Group& group);
virtual void apply(osg::LOD& lod);
virtual void apply(osg::Switch& switchNode);
void removeEmptyNodes(); void removeEmptyNodes();

View file

@ -109,23 +109,29 @@ void registerSerializers()
const char* ignore[] = { const char* ignore[] = {
"MWRender::PtrHolder", "MWRender::PtrHolder",
"Resource::TemplateRef", "Resource::TemplateRef",
"SceneUtil::CompositeStateSetUpdater",
"SceneUtil::LightListCallback", "SceneUtil::LightListCallback",
"SceneUtil::LightManagerUpdateCallback", "SceneUtil::LightManagerUpdateCallback",
"SceneUtil::UpdateRigBounds", "SceneUtil::UpdateRigBounds",
"SceneUtil::UpdateRigGeometry", "SceneUtil::UpdateRigGeometry",
"SceneUtil::LightSource", "SceneUtil::LightSource",
"SceneUtil::StateSetUpdater", "SceneUtil::StateSetUpdater",
"SceneUtil::DisableLight",
"SceneUtil::MWShadowTechnique",
"NifOsg::NodeUserData", "NifOsg::NodeUserData",
"NifOsg::FlipController", "NifOsg::FlipController",
"NifOsg::KeyframeController", "NifOsg::KeyframeController",
"NifOsg::TextKeyMapHolder", "NifOsg::TextKeyMapHolder",
"NifOsg::Emitter", "NifOsg::Emitter",
"NifOsg::ParticleColorAffector",
"NifOsg::ParticleSystem", "NifOsg::ParticleSystem",
"NifOsg::GravityAffector",
"NifOsg::GrowFadeAffector", "NifOsg::GrowFadeAffector",
"NifOsg::InverseWorldMatrix", "NifOsg::InverseWorldMatrix",
"NifOsg::StaticBoundingBoxCallback", "NifOsg::StaticBoundingBoxCallback",
"NifOsg::GeomMorpherController", "NifOsg::GeomMorpherController",
"NifOsg::UpdateMorphGeometry", "NifOsg::UpdateMorphGeometry",
"NifOsg::CollisionSwitch",
"osgMyGUI::Drawable", "osgMyGUI::Drawable",
"osg::DrawCallback", "osg::DrawCallback",
"osgOQ::ClearQueriesCallback", "osgOQ::ClearQueriesCallback",

View file

@ -40,6 +40,8 @@ namespace SceneUtil
mShadowSettings->setMinimumShadowMapNearFarRatio(Settings::Manager::getFloat("minimum lispsm near far ratio", "Shadows")); mShadowSettings->setMinimumShadowMapNearFarRatio(Settings::Manager::getFloat("minimum lispsm near far ratio", "Shadows"));
if (Settings::Manager::getBool("compute tight scene bounds", "Shadows")) if (Settings::Manager::getBool("compute tight scene bounds", "Shadows"))
mShadowSettings->setComputeNearFarModeOverride(osg::CullSettings::COMPUTE_NEAR_FAR_USING_PRIMITIVES); mShadowSettings->setComputeNearFarModeOverride(osg::CullSettings::COMPUTE_NEAR_FAR_USING_PRIMITIVES);
else
mShadowSettings->setComputeNearFarModeOverride(osg::CullSettings::COMPUTE_NEAR_FAR_USING_BOUNDING_VOLUMES);
int mapres = Settings::Manager::getInt("shadow map resolution", "Shadows"); int mapres = Settings::Manager::getInt("shadow map resolution", "Shadows");
mShadowSettings->setTextureSize(osg::Vec2s(mapres, mapres)); mShadowSettings->setTextureSize(osg::Vec2s(mapres, mapres));
@ -95,6 +97,7 @@ namespace SceneUtil
mShadowedScene->addChild(sceneRoot); mShadowedScene->addChild(sceneRoot);
rootNode->addChild(mShadowedScene); rootNode->addChild(mShadowedScene);
mShadowedScene->setNodeMask(sceneRoot->getNodeMask());
mShadowSettings = mShadowedScene->getShadowSettings(); mShadowSettings = mShadowedScene->getShadowSettings();
setupShadowSettings(); setupShadowSettings();

View file

@ -7,6 +7,12 @@
namespace SceneUtil namespace SceneUtil
{ {
// disable nonsense test against a worldsize bb what will always pass
class WaterBoundCallback : public osg::Drawable::ComputeBoundingBoxCallback
{
virtual osg::BoundingBox computeBound(const osg::Drawable&) const { return osg::BoundingBox(); }
};
osg::ref_ptr<osg::Geometry> createWaterGeometry(float size, int segments, float textureRepeats) osg::ref_ptr<osg::Geometry> createWaterGeometry(float size, int segments, float textureRepeats)
{ {
osg::ref_ptr<osg::Vec3Array> verts (new osg::Vec3Array); osg::ref_ptr<osg::Vec3Array> verts (new osg::Vec3Array);
@ -51,6 +57,8 @@ namespace SceneUtil
waterGeom->setNormalArray(normal, osg::Array::BIND_OVERALL); waterGeom->setNormalArray(normal, osg::Array::BIND_OVERALL);
waterGeom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,verts->size())); waterGeom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,verts->size()));
waterGeom->setComputeBoundingBoxCallback(new WaterBoundCallback);
waterGeom->setCullingActive(false);
return waterGeom; return waterGeom;
} }
@ -74,6 +82,9 @@ namespace SceneUtil
stateset->setRenderBinDetails(renderBin, "RenderBin"); stateset->setRenderBinDetails(renderBin, "RenderBin");
// Let the shader know we're dealing with simple water here.
stateset->addUniform(new osg::Uniform("simpleWater", true));
return stateset; return stateset;
} }
} }

View file

@ -21,6 +21,7 @@ void SceneUtil::writeScene(osg::Node *node, const std::string& filename, const s
osg::ref_ptr<osgDB::Options> options = new osgDB::Options; osg::ref_ptr<osgDB::Options> options = new osgDB::Options;
options->setPluginStringData("fileType", format); options->setPluginStringData("fileType", format);
options->setPluginStringData("WriteImageHint", "UseExternal");
rw->writeNode(*node, stream, options); rw->writeNode(*node, stream, options);
} }

View file

@ -58,7 +58,7 @@ namespace Shader
return true; return true;
} }
bool parseIncludes(boost::filesystem::path shaderPath, std::string& source, const std::string& shaderTemplate) bool parseIncludes(boost::filesystem::path shaderPath, std::string& source, const std::string& templateName)
{ {
Misc::StringUtils::replaceAll(source, "\r\n", "\n"); Misc::StringUtils::replaceAll(source, "\r\n", "\n");
@ -70,13 +70,13 @@ namespace Shader
size_t start = source.find('"', foundPos); size_t start = source.find('"', foundPos);
if (start == std::string::npos || start == source.size()-1) if (start == std::string::npos || start == source.size()-1)
{ {
Log(Debug::Error) << "Shader " << shaderTemplate << " error: Invalid #include"; Log(Debug::Error) << "Shader " << templateName << " error: Invalid #include";
return false; return false;
} }
size_t end = source.find('"', start+1); size_t end = source.find('"', start+1);
if (end == std::string::npos) if (end == std::string::npos)
{ {
Log(Debug::Error) << "Shader " << shaderTemplate << " error: Invalid #include"; Log(Debug::Error) << "Shader " << templateName << " error: Invalid #include";
return false; return false;
} }
std::string includeFilename = source.substr(start+1, end-(start+1)); std::string includeFilename = source.substr(start+1, end-(start+1));
@ -85,7 +85,7 @@ namespace Shader
includeFstream.open(includePath); includeFstream.open(includePath);
if (includeFstream.fail()) if (includeFstream.fail())
{ {
Log(Debug::Error) << "Shader " << shaderTemplate << " error: Failed to open include " << includePath.string(); Log(Debug::Error) << "Shader " << templateName << " error: Failed to open include " << includePath.string();
return false; return false;
} }
@ -120,14 +120,14 @@ namespace Shader
if (includedFiles.insert(includePath).second == false) if (includedFiles.insert(includePath).second == false)
{ {
Log(Debug::Error) << "Shader " << shaderTemplate << " error: Detected cyclic #includes"; Log(Debug::Error) << "Shader " << templateName << " error: Detected cyclic #includes";
return false; return false;
} }
} }
return true; return true;
} }
bool parseFors(std::string& source, const std::string& shaderTemplate) bool parseFors(std::string& source, const std::string& templateName)
{ {
const char escapeCharacter = '$'; const char escapeCharacter = '$';
size_t foundPos = 0; size_t foundPos = 0;
@ -136,13 +136,13 @@ namespace Shader
size_t endPos = source.find_first_of(" \n\r()[].;,", foundPos); size_t endPos = source.find_first_of(" \n\r()[].;,", foundPos);
if (endPos == std::string::npos) if (endPos == std::string::npos)
{ {
Log(Debug::Error) << "Shader " << shaderTemplate << " error: Unexpected EOF"; Log(Debug::Error) << "Shader " << templateName << " error: Unexpected EOF";
return false; return false;
} }
std::string command = source.substr(foundPos + 1, endPos - (foundPos + 1)); std::string command = source.substr(foundPos + 1, endPos - (foundPos + 1));
if (command != "foreach") if (command != "foreach")
{ {
Log(Debug::Error) << "Shader " << shaderTemplate << " error: Unknown shader directive: $" << command; Log(Debug::Error) << "Shader " << templateName << " error: Unknown shader directive: $" << command;
return false; return false;
} }
@ -150,7 +150,7 @@ namespace Shader
size_t iterNameEnd = source.find_first_of(" \n\r()[].;,", iterNameStart); size_t iterNameEnd = source.find_first_of(" \n\r()[].;,", iterNameStart);
if (iterNameEnd == std::string::npos) if (iterNameEnd == std::string::npos)
{ {
Log(Debug::Error) << "Shader " << shaderTemplate << " error: Unexpected EOF"; Log(Debug::Error) << "Shader " << templateName << " error: Unexpected EOF";
return false; return false;
} }
std::string iteratorName = "$" + source.substr(iterNameStart, iterNameEnd - iterNameStart); std::string iteratorName = "$" + source.substr(iterNameStart, iterNameEnd - iterNameStart);
@ -159,7 +159,7 @@ namespace Shader
size_t listEnd = source.find_first_of("\n\r", listStart); size_t listEnd = source.find_first_of("\n\r", listStart);
if (listEnd == std::string::npos) if (listEnd == std::string::npos)
{ {
Log(Debug::Error) << "Shader " << shaderTemplate << " error: Unexpected EOF"; Log(Debug::Error) << "Shader " << templateName << " error: Unexpected EOF";
return false; return false;
} }
std::string list = source.substr(listStart, listEnd - listStart); std::string list = source.substr(listStart, listEnd - listStart);
@ -171,7 +171,7 @@ namespace Shader
size_t contentEnd = source.find("$endforeach", contentStart); size_t contentEnd = source.find("$endforeach", contentStart);
if (contentEnd == std::string::npos) if (contentEnd == std::string::npos)
{ {
Log(Debug::Error) << "Shader " << shaderTemplate << " error: Unexpected EOF"; Log(Debug::Error) << "Shader " << templateName << " error: Unexpected EOF";
return false; return false;
} }
std::string content = source.substr(contentStart, contentEnd - contentStart); std::string content = source.substr(contentStart, contentEnd - contentStart);
@ -211,7 +211,7 @@ namespace Shader
} }
bool parseDefines(std::string& source, const ShaderManager::DefineMap& defines, bool parseDefines(std::string& source, const ShaderManager::DefineMap& defines,
const ShaderManager::DefineMap& globalDefines, const std::string& shaderTemplate) const ShaderManager::DefineMap& globalDefines, const std::string& templateName)
{ {
const char escapeCharacter = '@'; const char escapeCharacter = '@';
size_t foundPos = 0; size_t foundPos = 0;
@ -221,7 +221,7 @@ namespace Shader
size_t endPos = source.find_first_of(" \n\r()[].;,", foundPos); size_t endPos = source.find_first_of(" \n\r()[].;,", foundPos);
if (endPos == std::string::npos) if (endPos == std::string::npos)
{ {
Log(Debug::Error) << "Shader " << shaderTemplate << " error: Unexpected EOF"; Log(Debug::Error) << "Shader " << templateName << " error: Unexpected EOF";
return false; return false;
} }
std::string define = source.substr(foundPos+1, endPos - (foundPos+1)); std::string define = source.substr(foundPos+1, endPos - (foundPos+1));
@ -234,7 +234,7 @@ namespace Shader
size_t iterNameEnd = source.find_first_of(" \n\r()[].;,", iterNameStart); size_t iterNameEnd = source.find_first_of(" \n\r()[].;,", iterNameStart);
if (iterNameEnd == std::string::npos) if (iterNameEnd == std::string::npos)
{ {
Log(Debug::Error) << "Shader " << shaderTemplate << " error: Unexpected EOF"; Log(Debug::Error) << "Shader " << templateName << " error: Unexpected EOF";
return false; return false;
} }
forIterators.push_back(source.substr(iterNameStart, iterNameEnd - iterNameStart)); forIterators.push_back(source.substr(iterNameStart, iterNameEnd - iterNameStart));
@ -244,7 +244,7 @@ namespace Shader
source.replace(foundPos, 1, "$"); source.replace(foundPos, 1, "$");
if (forIterators.empty()) if (forIterators.empty())
{ {
Log(Debug::Error) << "Shader " << shaderTemplate << " error: endforeach without foreach"; Log(Debug::Error) << "Shader " << templateName << " error: endforeach without foreach";
return false; return false;
} }
else else
@ -264,22 +264,22 @@ namespace Shader
} }
else else
{ {
Log(Debug::Error) << "Shader " << shaderTemplate << " error: Undefined " << define; Log(Debug::Error) << "Shader " << templateName << " error: Undefined " << define;
return false; return false;
} }
} }
return true; return true;
} }
osg::ref_ptr<osg::Shader> ShaderManager::getShader(const std::string &shaderTemplate, const ShaderManager::DefineMap &defines, osg::Shader::Type shaderType) osg::ref_ptr<osg::Shader> ShaderManager::getShader(const std::string &templateName, const ShaderManager::DefineMap &defines, osg::Shader::Type shaderType)
{ {
OpenThreads::ScopedLock<OpenThreads::Mutex> lock(mMutex); OpenThreads::ScopedLock<OpenThreads::Mutex> lock(mMutex);
// read the template if we haven't already // read the template if we haven't already
TemplateMap::iterator templateIt = mShaderTemplates.find(shaderTemplate); TemplateMap::iterator templateIt = mShaderTemplates.find(templateName);
if (templateIt == mShaderTemplates.end()) if (templateIt == mShaderTemplates.end())
{ {
boost::filesystem::path p = (boost::filesystem::path(mPath) / shaderTemplate); boost::filesystem::path p = (boost::filesystem::path(mPath) / templateName);
boost::filesystem::ifstream stream; boost::filesystem::ifstream stream;
stream.open(p); stream.open(p);
if (stream.fail()) if (stream.fail())
@ -293,20 +293,20 @@ namespace Shader
// parse includes // parse includes
std::string source = buffer.str(); std::string source = buffer.str();
if (!addLineDirectivesAfterConditionalBlocks(source) if (!addLineDirectivesAfterConditionalBlocks(source)
|| !parseIncludes(boost::filesystem::path(mPath), source, shaderTemplate)) || !parseIncludes(boost::filesystem::path(mPath), source, templateName))
return nullptr; return nullptr;
templateIt = mShaderTemplates.insert(std::make_pair(shaderTemplate, source)).first; templateIt = mShaderTemplates.insert(std::make_pair(templateName, source)).first;
} }
ShaderMap::iterator shaderIt = mShaders.find(std::make_pair(shaderTemplate, defines)); ShaderMap::iterator shaderIt = mShaders.find(std::make_pair(templateName, defines));
if (shaderIt == mShaders.end()) if (shaderIt == mShaders.end())
{ {
std::string shaderSource = templateIt->second; std::string shaderSource = templateIt->second;
if (!parseDefines(shaderSource, defines, mGlobalDefines, shaderTemplate) || !parseFors(shaderSource, shaderTemplate)) if (!parseDefines(shaderSource, defines, mGlobalDefines, templateName) || !parseFors(shaderSource, templateName))
{ {
// Add to the cache anyway to avoid logging the same error over and over. // Add to the cache anyway to avoid logging the same error over and over.
mShaders.insert(std::make_pair(std::make_pair(shaderTemplate, defines), nullptr)); mShaders.insert(std::make_pair(std::make_pair(templateName, defines), nullptr));
return nullptr; return nullptr;
} }
@ -316,7 +316,7 @@ namespace Shader
static unsigned int counter = 0; static unsigned int counter = 0;
shader->setName(std::to_string(counter++)); shader->setName(std::to_string(counter++));
shaderIt = mShaders.insert(std::make_pair(std::make_pair(shaderTemplate, defines), shader)).first; shaderIt = mShaders.insert(std::make_pair(std::make_pair(templateName, defines), shader)).first;
} }
return shaderIt->second; return shaderIt->second;
} }
@ -372,4 +372,14 @@ namespace Shader
program.second->releaseGLObjects(state); program.second->releaseGLObjects(state);
} }
const osg::ref_ptr<osg::Uniform> ShaderManager::getShadowMapAlphaTestEnableUniform()
{
return mShadowMapAlphaTestEnableUniform;
}
const osg::ref_ptr<osg::Uniform> ShaderManager::getShadowMapAlphaTestDisableUniform()
{
return mShadowMapAlphaTestDisableUniform;
}
} }

View file

@ -30,7 +30,7 @@ namespace Shader
/// @param shaderType The type of shader (usually vertex or fragment shader). /// @param shaderType The type of shader (usually vertex or fragment shader).
/// @note May return nullptr on failure. /// @note May return nullptr on failure.
/// @note Thread safe. /// @note Thread safe.
osg::ref_ptr<osg::Shader> getShader(const std::string& shaderTemplate, const DefineMap& defines, osg::Shader::Type shaderType); osg::ref_ptr<osg::Shader> getShader(const std::string& templateName, const DefineMap& defines, osg::Shader::Type shaderType);
osg::ref_ptr<osg::Program> getProgram(osg::ref_ptr<osg::Shader> vertexShader, osg::ref_ptr<osg::Shader> fragmentShader); osg::ref_ptr<osg::Program> getProgram(osg::ref_ptr<osg::Shader> vertexShader, osg::ref_ptr<osg::Shader> fragmentShader);
@ -44,6 +44,9 @@ namespace Shader
void releaseGLObjects(osg::State* state); void releaseGLObjects(osg::State* state);
const osg::ref_ptr<osg::Uniform> getShadowMapAlphaTestEnableUniform();
const osg::ref_ptr<osg::Uniform> getShadowMapAlphaTestDisableUniform();
private: private:
std::string mPath; std::string mPath;
@ -61,8 +64,15 @@ namespace Shader
ProgramMap mPrograms; ProgramMap mPrograms;
OpenThreads::Mutex mMutex; OpenThreads::Mutex mMutex;
const osg::ref_ptr<osg::Uniform> mShadowMapAlphaTestEnableUniform = new osg::Uniform();
const osg::ref_ptr<osg::Uniform> mShadowMapAlphaTestDisableUniform = new osg::Uniform();
}; };
bool parseFors(std::string& source, const std::string& templateName);
bool parseDefines(std::string& source, const ShaderManager::DefineMap& defines,
const ShaderManager::DefineMap& globalDefines, const std::string& templateName);
} }
#endif #endif

View file

@ -1,8 +1,10 @@
#include "shadervisitor.hpp" #include "shadervisitor.hpp"
#include <osg/Texture> #include <osg/AlphaFunc>
#include <osg/Material> #include <osg/BlendFunc>
#include <osg/Geometry> #include <osg/Geometry>
#include <osg/Material>
#include <osg/Texture>
#include <osgUtil/TangentSpaceGenerator> #include <osgUtil/TangentSpaceGenerator>
@ -23,6 +25,7 @@ namespace Shader
: mShaderRequired(false) : mShaderRequired(false)
, mColorMode(0) , mColorMode(0)
, mMaterialOverridden(false) , mMaterialOverridden(false)
, mBlendFuncOverridden(false)
, mNormalHeight(false) , mNormalHeight(false)
, mTexStageRequiringTangents(-1) , mTexStageRequiringTangents(-1)
, mNode(nullptr) , mNode(nullptr)
@ -229,15 +232,21 @@ namespace Shader
{ {
if (!writableStateSet) if (!writableStateSet)
writableStateSet = getWritableStateSet(node); writableStateSet = getWritableStateSet(node);
// We probably shouldn't construct a new version of this each time as Uniforms use pointer comparison for early-out.
// Also it should probably belong to the shader manager
writableStateSet->addUniform(new osg::Uniform("useDiffuseMapForShadowAlpha", true)); writableStateSet->addUniform(new osg::Uniform("useDiffuseMapForShadowAlpha", true));
} }
} }
bool alphaSettingsChanged = false;
bool alphaTestShadows = false;
const osg::StateSet::AttributeList& attributes = stateset->getAttributeList(); const osg::StateSet::AttributeList& attributes = stateset->getAttributeList();
for (osg::StateSet::AttributeList::const_iterator it = attributes.begin(); it != attributes.end(); ++it) for (osg::StateSet::AttributeList::const_iterator it = attributes.begin(); it != attributes.end(); ++it)
{ {
if (it->first.first == osg::StateAttribute::MATERIAL) if (it->first.first == osg::StateAttribute::MATERIAL)
{ {
// This should probably be moved out of ShaderRequirements and be applied directly now it's a uniform instead of a define
if (!mRequirements.back().mMaterialOverridden || it->second.second & osg::StateAttribute::PROTECTED) if (!mRequirements.back().mMaterialOverridden || it->second.second & osg::StateAttribute::PROTECTED)
{ {
if (it->second.second & osg::StateAttribute::OVERRIDE) if (it->second.second & osg::StateAttribute::OVERRIDE)
@ -269,6 +278,27 @@ namespace Shader
mRequirements.back().mColorMode = colorMode; mRequirements.back().mColorMode = colorMode;
} }
} }
else if (it->first.first == osg::StateAttribute::BLENDFUNC)
{
if (!mRequirements.back().mBlendFuncOverridden || it->second.second & osg::StateAttribute::PROTECTED)
{
if (it->second.second & osg::StateAttribute::OVERRIDE)
mRequirements.back().mBlendFuncOverridden = true;
const osg::BlendFunc* blend = static_cast<const osg::BlendFunc*>(it->second.first.get());
if (blend->getSource() == osg::BlendFunc::SRC_ALPHA || blend->getSource() == osg::BlendFunc::SRC_COLOR)
alphaTestShadows = true;
alphaSettingsChanged = true;
}
}
// Eventually, move alpha testing to discard in shader adn remove deprecated state here
}
// we don't need to check for glEnable/glDisable of blending as we always set it at the same time
if (alphaSettingsChanged)
{
if (!writableStateSet)
writableStateSet = getWritableStateSet(node);
writableStateSet->addUniform(alphaTestShadows ? mShaderManager.getShadowMapAlphaTestEnableUniform() : mShaderManager.getShadowMapAlphaTestDisableUniform());
} }
} }

View file

@ -75,6 +75,8 @@ namespace Shader
int mColorMode; int mColorMode;
bool mMaterialOverridden; bool mMaterialOverridden;
bool mBlendFuncOverridden;
bool mNormalHeight; // true if normal map has height info in alpha channel bool mNormalHeight; // true if normal map has height info in alpha channel
// -1 == no tangents required // -1 == no tangents required

View file

@ -323,6 +323,7 @@ void updateWaterCullingView(HeightCullCallback* callback, ViewData* vd, osgUtil:
return; return;
} }
cv->pushCurrentMask(); cv->pushCurrentMask();
static bool debug = getenv("OPENMW_WATER_CULLING_DEBUG") != nullptr;
for (unsigned int i=0; i<vd->getNumEntries(); ++i) for (unsigned int i=0; i<vd->getNumEntries(); ++i)
{ {
ViewData::Entry& entry = vd->getEntry(i); ViewData::Entry& entry = vd->getEntry(i);
@ -337,7 +338,6 @@ void updateWaterCullingView(HeightCullCallback* callback, ViewData* vd, osgUtil:
continue; continue;
lowZ = bb._min.z(); lowZ = bb._min.z();
static bool debug = getenv("OPENMW_WATER_CULLING_DEBUG") != nullptr;
if (!debug) if (!debug)
break; break;
osg::Box* b = new osg::Box; osg::Box* b = new osg::Box;

View file

@ -40,36 +40,46 @@ namespace Terrain
class ChunkManager; class ChunkManager;
class CompositeMapRenderer; class CompositeMapRenderer;
class HeightCullCallback : public osg::NodeCallback class HeightCullCallback : public osg::NodeCallback
{
public:
HeightCullCallback() : mLowZ(-std::numeric_limits<float>::max()), mHighZ(std::numeric_limits<float>::max()), mMask(~0) {}
void setLowZ(float z)
{ {
mLowZ = z; public:
} void setLowZ(float z)
float getLowZ() const { return mLowZ; } {
mLowZ = z;
}
float getLowZ() const
{
return mLowZ;
}
void setHighZ(float highZ) void setHighZ(float highZ)
{ {
mHighZ = highZ; mHighZ = highZ;
} }
float getHighZ() const { return mHighZ; } float getHighZ() const
{
return mHighZ;
}
void setCullMask(unsigned int mask) { mMask = mask; } void setCullMask(unsigned int mask)
unsigned int getCullMask() const { return mMask; } {
mMask = mask;
}
unsigned int getCullMask() const
{
return mMask;
}
virtual void operator()(osg::Node* node, osg::NodeVisitor* nv) virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
{ {
if (mLowZ <= mHighZ) if (mLowZ <= mHighZ)
traverse(node, nv); traverse(node, nv);
} }
private: private:
float mLowZ; float mLowZ{-std::numeric_limits<float>::max()};
float mHighZ; float mHighZ{std::numeric_limits<float>::max()};
unsigned int mMask; unsigned int mMask{~0u};
}; };
/** /**
* @brief A View is a collection of rendering objects that are visible from a given camera/intersection. * @brief A View is a collection of rendering objects that are visible from a given camera/intersection.

View file

@ -74,6 +74,20 @@ Game will not eat all memory at once.
Memory will be consumed in approximately linear dependency from number of nav mesh updates. Memory will be consumed in approximately linear dependency from number of nav mesh updates.
But only for new locations or already dropped from cache. But only for new locations or already dropped from cache.
min update interval ms
----------------
:Type: integer
:Range: >= 0
:Default: 250
Minimum time duration required to pass before next navmesh update for the same tile in milliseconds.
Only tiles affected where objects are transformed.
Next update for tile with added or removed object will not be delayed.
Visible ingame effect is navmesh update around opening or closing door.
Primary usage is for rotating signs like in Seyda Neen at Arrille's Tradehouse entrance.
Decreasing this value may increase CPU usage by background threads.
Developer's settings Developer's settings
******************** ********************

View file

@ -88,7 +88,6 @@ compute tight scene bounds
:Default: True :Default: True
With this setting enabled, attempt to better use the shadow map(s) by making them cover a smaller area. With this setting enabled, attempt to better use the shadow map(s) by making them cover a smaller area.
This can be especially helpful when looking downwards with a high viewing distance but will be less useful with the default value.
May have a minor to major performance impact. May have a minor to major performance impact.
shadow map resolution shadow map resolution

View file

@ -119,6 +119,13 @@ bool GLWidget::event( QEvent* event )
enqueueDeferredEvent(QEvent::ParentChange); enqueueDeferredEvent(QEvent::ParentChange);
return true; return true;
} }
#if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)
else if (event->type() == QEvent::PlatformSurface && static_cast<QPlatformSurfaceEvent*>(event)->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed)
{
if (_gw)
_gw->close();
}
#endif
// perform regular event handling // perform regular event handling
return QGLWidget::event( event ); return QGLWidget::event( event );

View file

@ -776,6 +776,9 @@ enable recast mesh render = false
# Max number of navmesh tiles (value >= 0) # Max number of navmesh tiles (value >= 0)
max tiles number = 512 max tiles number = 512
# Min time duration for the same tile update in milliseconds (value >= 0)
min update interval ms = 250
[Shadows] [Shadows]
# Enable or disable shadows. Bear in mind that this will force OpenMW to use shaders as if "[Shaders]/force shaders" was set to true. # Enable or disable shadows. Bear in mind that this will force OpenMW to use shaders as if "[Shaders]/force shaders" was set to true.
@ -805,7 +808,7 @@ enable debug hud = false
# Enable the debug overlay to see where each shadow map affects. # Enable the debug overlay to see where each shadow map affects.
enable debug overlay = false enable debug overlay = false
# Attempt to better use the shadow map by making them cover a smaller area. Especially helpful when looking downwards. May have a minor to major performance impact. # Attempt to better use the shadow map by making them cover a smaller area. May have a minor to major performance impact.
compute tight scene bounds = true compute tight scene bounds = true
# How large to make the shadow map(s). Higher values increase GPU load, but can produce better-looking results. Power-of-two values may turn out to be faster on some GPU/driver combinations. # How large to make the shadow map(s). Higher values increase GPU load, but can produce better-looking results. Power-of-two values may turn out to be faster on some GPU/driver combinations.

View file

@ -49,6 +49,8 @@ uniform vec2 envMapLumaBias;
uniform mat2 bumpMapMatrix; uniform mat2 bumpMapMatrix;
#endif #endif
uniform bool simpleWater = false;
varying float euclideanDepth; varying float euclideanDepth;
varying float linearDepth; varying float linearDepth;
@ -90,7 +92,7 @@ void main()
vec3 cameraPos = (gl_ModelViewMatrixInverse * vec4(0,0,0,1)).xyz; vec3 cameraPos = (gl_ModelViewMatrixInverse * vec4(0,0,0,1)).xyz;
vec3 objectPos = (gl_ModelViewMatrixInverse * vec4(passViewPos, 1)).xyz; vec3 objectPos = (gl_ModelViewMatrixInverse * vec4(passViewPos, 1)).xyz;
vec3 eyeDir = normalize(cameraPos - objectPos); vec3 eyeDir = normalize(cameraPos - objectPos);
vec2 offset = getParallaxOffset(eyeDir, tbnTranspose, normalTex.a, (passTangent.w > 0) ? -1.f : 1.f); vec2 offset = getParallaxOffset(eyeDir, tbnTranspose, normalTex.a, (passTangent.w > 0.0) ? -1.f : 1.f);
adjustedDiffuseUV += offset; // only offset diffuse for now, other textures are more likely to be using a completely different UV set adjustedDiffuseUV += offset; // only offset diffuse for now, other textures are more likely to be using a completely different UV set
// TODO: check not working as the same UV buffer is being bound to different targets // TODO: check not working as the same UV buffer is being bound to different targets
@ -171,7 +173,7 @@ void main()
#if @specularMap #if @specularMap
vec4 specTex = texture2D(specularMap, specularMapUV); vec4 specTex = texture2D(specularMap, specularMapUV);
float shininess = specTex.a * 255; float shininess = specTex.a * 255.0;
vec3 matSpec = specTex.xyz; vec3 matSpec = specTex.xyz;
#else #else
float shininess = gl_FrontMaterial.shininess; float shininess = gl_FrontMaterial.shininess;
@ -180,7 +182,11 @@ void main()
gl_FragData[0].xyz += getSpecular(normalize(viewNormal), normalize(passViewPos.xyz), shininess, matSpec) * shadowing; gl_FragData[0].xyz += getSpecular(normalize(viewNormal), normalize(passViewPos.xyz), shininess, matSpec) * shadowing;
#if @radialFog #if @radialFog
float fogValue = clamp((euclideanDepth - gl_Fog.start) * gl_Fog.scale, 0.0, 1.0); float depth = euclideanDepth;
// For the less detailed mesh of simple water we need to recalculate depth on per-pixel basis
if (simpleWater)
depth = length(passViewPos);
float fogValue = clamp((depth - gl_Fog.start) * gl_Fog.scale, 0.0, 1.0);
#else #else
float fogValue = clamp((linearDepth - gl_Fog.start) * gl_Fog.scale, 0.0, 1.0); float fogValue = clamp((linearDepth - gl_Fog.start) * gl_Fog.scale, 0.0, 1.0);
#endif #endif

View file

@ -6,6 +6,7 @@ varying vec2 diffuseMapUV;
varying float alphaPassthrough; varying float alphaPassthrough;
uniform bool useDiffuseMapForShadowAlpha; uniform bool useDiffuseMapForShadowAlpha;
uniform bool alphaTestShadows;
void main() void main()
{ {
@ -15,7 +16,7 @@ void main()
else else
gl_FragData[0].a = alphaPassthrough; gl_FragData[0].a = alphaPassthrough;
// Prevent translucent things casting shadow (including the player using an invisibility effect) // Prevent translucent things casting shadow (including the player using an invisibility effect). For now, rely on the deprecated FF test for non-blended stuff.
if (gl_FragData[0].a <= 0.5) if (alphaTestShadows && gl_FragData[0].a <= 0.5)
discard; discard;
} }

View file

@ -5,7 +5,8 @@ varying vec2 diffuseMapUV;
varying float alphaPassthrough; varying float alphaPassthrough;
uniform int colorMode; uniform int colorMode;
uniform bool useDiffuseMapForShadowAlpha; uniform bool useDiffuseMapForShadowAlpha = true;
uniform bool alphaTestShadows = true;
void main(void) void main(void)
{ {
@ -18,7 +19,6 @@ void main(void)
diffuseMapUV = (gl_TextureMatrix[0] * gl_MultiTexCoord0).xy; diffuseMapUV = (gl_TextureMatrix[0] * gl_MultiTexCoord0).xy;
else else
diffuseMapUV = vec2(0.0); // Avoid undefined behaviour if running on hardware predating the concept of dynamically uniform expressions diffuseMapUV = vec2(0.0); // Avoid undefined behaviour if running on hardware predating the concept of dynamically uniform expressions
if (colorMode == 2) if (colorMode == 2)
alphaPassthrough = gl_Color.a; alphaPassthrough = gl_Color.a;
else else

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